Add the provider contract, error taxonomy, naming, and config (Step 2)
The Provider interface (Create/Get/Delete/ListByTag), Instance, and CreateRequest that every cloud backend implements — kept independent of api/v1alpha1 so this package has no CRD-type coupling. Error taxonomy (ErrNotFound/ErrQuotaExceeded/ErrTransient/ErrPermanent) wrapped via a multi-error Unwrap() []error, so errors.Is and errors.As both work off the same value: the reconciler branches on classification, logs keep the underlying SDK error. Unclassified errors default to ErrTransient — retrying is always safer than latching Failed. Deterministic instance naming (SHA-256 -> base32 -> 16 chars, 22 total with the "proxy-" prefix) satisfying GCP's RFC1035 name rules with headroom, and idempotency-tested across 10k UIDs with zero collisions. --providers-config YAML parsing (config.go) with fail-fast validation: unknown type, duplicate name, missing gcp.project, mismatched type/config-block, and strict-mode rejection of unknown keys. internal/provider/registry/registry.go takes its type->constructor map as a parameter rather than hardcoding it, so the package has zero import on internal/provider/mock or internal/provider/gcp (neither exists yet — mock is Step 3, gcp is Step 8) and compiles today. Explicit wiring moves to the composition root in cmd/main.go (Step 10). Deferred internal/provider/metrics.go (the WithMetrics decorator) to Step 9, where the Prometheus vectors it needs actually get built — nothing in this step depends on it. internal/provider at 96.2% coverage, internal/provider/registry at 100%. make test green. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
70
internal/provider/errors_test.go
Normal file
70
internal/provider/errors_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// sdkError simulates an underlying provider SDK error type, so tests can
|
||||
// assert errors.As reaches through the wrapper to it.
|
||||
type sdkError struct{ code int }
|
||||
|
||||
func (e *sdkError) Error() string { return fmt.Sprintf("sdk error %d", e.code) }
|
||||
|
||||
func TestError_IsAndAs(t *testing.T) {
|
||||
t.Parallel()
|
||||
underlying := &sdkError{code: 429}
|
||||
err := Wrap(ErrQuotaExceeded, "create", "gcp-eu", "zones/z/instances/x", underlying)
|
||||
|
||||
if !errors.Is(err, ErrQuotaExceeded) {
|
||||
t.Error("errors.Is(err, ErrQuotaExceeded) = false, want true")
|
||||
}
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
t.Error("errors.Is(err, ErrNotFound) = true, want false")
|
||||
}
|
||||
|
||||
var sdk *sdkError
|
||||
if !errors.As(err, &sdk) {
|
||||
t.Fatal("errors.As(err, &sdk) = false, want true")
|
||||
}
|
||||
if sdk.code != 429 {
|
||||
t.Errorf("recovered sdkError.code = %d, want 429", sdk.code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestError_WrapWithNilUnderlying(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := Wrap(ErrPermanent, "get", "mock", "id", nil)
|
||||
if !errors.Is(err, ErrPermanent) {
|
||||
t.Error("errors.Is(err, ErrPermanent) = false, want true")
|
||||
}
|
||||
if err.Error() == "" {
|
||||
t.Error("Error() returned an empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClass(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want error
|
||||
}{
|
||||
{"nil returns nil", nil, nil},
|
||||
{"wrapped not found", Wrap(ErrNotFound, "get", "mock", "id", nil), ErrNotFound},
|
||||
{"wrapped quota", Wrap(ErrQuotaExceeded, "create", "gcp", "id", nil), ErrQuotaExceeded},
|
||||
{"wrapped permanent", Wrap(ErrPermanent, "create", "gcp", "id", nil), ErrPermanent},
|
||||
{"wrapped transient", Wrap(ErrTransient, "create", "gcp", "id", nil), ErrTransient},
|
||||
{"unclassified defaults to transient", errors.New("boom"), ErrTransient},
|
||||
{"further-wrapped preserves classification", fmt.Errorf("outer: %w", Wrap(ErrQuotaExceeded, "create", "gcp", "id", nil)), ErrQuotaExceeded},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := Class(tc.err); got != tc.want {
|
||||
t.Errorf("Class(%v) = %v, want %v", tc.err, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user