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:
173
internal/provider/config_test.go
Normal file
173
internal/provider/config_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfig_valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
data := []byte(`
|
||||
providers:
|
||||
- name: mock
|
||||
type: mock
|
||||
mock:
|
||||
provisionDelaySeconds: 2
|
||||
- name: gcp-eu
|
||||
type: gcp
|
||||
gcp:
|
||||
project: my-project
|
||||
network: custom-net
|
||||
`)
|
||||
cfg, err := LoadConfig(data)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v, want nil", err)
|
||||
}
|
||||
if len(cfg.Providers) != 2 {
|
||||
t.Fatalf("len(cfg.Providers) = %d, want 2", len(cfg.Providers))
|
||||
}
|
||||
if cfg.Providers[0].Mock == nil || cfg.Providers[0].Mock.ProvisionDelaySeconds != 2 {
|
||||
t.Errorf("providers[0].mock = %+v, want ProvisionDelaySeconds=2", cfg.Providers[0].Mock)
|
||||
}
|
||||
if cfg.Providers[1].GCP == nil || cfg.Providers[1].GCP.Project != "my-project" {
|
||||
t.Errorf("providers[1].gcp = %+v, want Project=my-project", cfg.Providers[1].GCP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_invalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
wantErrSub string
|
||||
}{
|
||||
{
|
||||
name: "empty providers list",
|
||||
yaml: `providers: []`,
|
||||
wantErrSub: "at least one provider",
|
||||
},
|
||||
{
|
||||
name: "missing name",
|
||||
yaml: `
|
||||
providers:
|
||||
- type: mock`,
|
||||
wantErrSub: "name is required",
|
||||
},
|
||||
{
|
||||
name: "duplicate name",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: mock
|
||||
type: mock
|
||||
- name: mock
|
||||
type: mock`,
|
||||
wantErrSub: "duplicate provider name",
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1
|
||||
type: azure`,
|
||||
wantErrSub: `unknown provider type "azure"`,
|
||||
},
|
||||
{
|
||||
name: "missing type",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1`,
|
||||
wantErrSub: "type is required",
|
||||
},
|
||||
{
|
||||
name: "gcp missing project",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: gcp-eu
|
||||
type: gcp`,
|
||||
wantErrSub: "gcp.project is required",
|
||||
},
|
||||
{
|
||||
name: "gcp with empty project",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: gcp-eu
|
||||
type: gcp
|
||||
gcp:
|
||||
project: ""`,
|
||||
wantErrSub: "gcp.project is required",
|
||||
},
|
||||
{
|
||||
name: "mock type with gcp block",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1
|
||||
type: mock
|
||||
gcp:
|
||||
project: my-project`,
|
||||
wantErrSub: "type is mock but a gcp block is set",
|
||||
},
|
||||
{
|
||||
name: "gcp type with mock block",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1
|
||||
type: gcp
|
||||
gcp:
|
||||
project: my-project
|
||||
mock:
|
||||
failNextCreates: 1`,
|
||||
wantErrSub: "type is gcp but a mock block is set",
|
||||
},
|
||||
{
|
||||
name: "unknown mock.failWith",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1
|
||||
type: mock
|
||||
mock:
|
||||
failWith: oops`,
|
||||
wantErrSub: `unknown mock.failWith "oops"`,
|
||||
},
|
||||
{
|
||||
name: "strict mode rejects unknown top-level key",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1
|
||||
type: mock
|
||||
extraneous: true`,
|
||||
wantErrSub: "parsing providers config",
|
||||
},
|
||||
{
|
||||
name: "strict mode rejects unknown provider key",
|
||||
yaml: `
|
||||
providers:
|
||||
- name: p1
|
||||
type: mock
|
||||
bogus: true`,
|
||||
wantErrSub: "parsing providers config",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := LoadConfig([]byte(tc.yaml))
|
||||
if err == nil {
|
||||
t.Fatalf("LoadConfig() error = nil, want error containing %q", tc.wantErrSub)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErrSub) {
|
||||
t.Errorf("LoadConfig() error = %q, want substring %q", err.Error(), tc.wantErrSub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFile_missingFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := LoadConfigFile("/nonexistent/providers.yaml")
|
||||
if err == nil {
|
||||
t.Fatal("LoadConfigFile() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "reading providers config") {
|
||||
t.Errorf("LoadConfigFile() error = %q, want substring %q", err.Error(), "reading providers config")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user