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:
92
internal/provider/registry/registry_test.go
Normal file
92
internal/provider/registry/registry_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
||||
)
|
||||
|
||||
// stubProvider is a minimal provider.Provider satisfying the interface for
|
||||
// registry tests, which care about wiring, not provider behavior.
|
||||
type stubProvider struct{ name string }
|
||||
|
||||
func (s *stubProvider) Create(ctx context.Context, req provider.CreateRequest) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (s *stubProvider) Get(ctx context.Context, providerID string) (*provider.Instance, error) {
|
||||
return nil, provider.ErrNotFound
|
||||
}
|
||||
func (s *stubProvider) Delete(ctx context.Context, providerID string) error { return nil }
|
||||
func (s *stubProvider) ListByTag(ctx context.Context) ([]provider.Instance, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func stubConstructor(name string) Constructor {
|
||||
return func(ctx context.Context, cfg provider.ProviderConfig) (provider.Provider, error) {
|
||||
return &stubProvider{name: name}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_dispatchesByType(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &provider.Config{Providers: []provider.ProviderConfig{
|
||||
{Name: "mock", Type: "mock"},
|
||||
{Name: "gcp-eu", Type: "gcp"},
|
||||
}}
|
||||
builtins := map[string]Constructor{
|
||||
"mock": stubConstructor("mock"),
|
||||
"gcp": stubConstructor("gcp"),
|
||||
}
|
||||
|
||||
providers, err := Build(context.Background(), cfg, builtins)
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v, want nil", err)
|
||||
}
|
||||
if len(providers) != 2 {
|
||||
t.Fatalf("len(providers) = %d, want 2", len(providers))
|
||||
}
|
||||
if _, ok := providers["mock"]; !ok {
|
||||
t.Error(`providers["mock"] missing`)
|
||||
}
|
||||
if _, ok := providers["gcp-eu"]; !ok {
|
||||
t.Error(`providers["gcp-eu"] missing, keyed by config name not type`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_unknownType(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &provider.Config{Providers: []provider.ProviderConfig{
|
||||
{Name: "p1", Type: "azure"},
|
||||
}}
|
||||
_, err := Build(context.Background(), cfg, map[string]Constructor{"mock": stubConstructor("mock")})
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want error for unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_constructorErrorAbortsWholeBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
failing := errors.New("boom")
|
||||
cfg := &provider.Config{Providers: []provider.ProviderConfig{
|
||||
{Name: "mock", Type: "mock"},
|
||||
{Name: "bad", Type: "bad"},
|
||||
}}
|
||||
builtins := map[string]Constructor{
|
||||
"mock": stubConstructor("mock"),
|
||||
"bad": func(ctx context.Context, cfg provider.ProviderConfig) (provider.Provider, error) {
|
||||
return nil, failing
|
||||
},
|
||||
}
|
||||
providers, err := Build(context.Background(), cfg, builtins)
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want error from failing constructor")
|
||||
}
|
||||
if !errors.Is(err, failing) {
|
||||
t.Errorf("Build() error = %v, want it to wrap %v", err, failing)
|
||||
}
|
||||
if providers != nil {
|
||||
t.Errorf("Build() providers = %v, want nil on failure (no partial result)", providers)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user