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>
71 lines
2.0 KiB
Go
71 lines
2.0 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|