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:
61
internal/provider/name_test.go
Normal file
61
internal/provider/name_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
var nameRegexp = regexp.MustCompile(`^proxy-[a-z2-7]{16}$`)
|
||||
|
||||
func TestNameFromUID_idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
uid := types.UID("f47ac10b-58cc-4372-a567-0e02b2c3d479")
|
||||
got1 := NameFromUID(uid)
|
||||
got2 := NameFromUID(uid)
|
||||
if got1 != got2 {
|
||||
t.Errorf("NameFromUID(%q) is not idempotent: %q != %q", uid, got1, got2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameFromUID_charsetAndLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []types.UID{
|
||||
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
"",
|
||||
"a",
|
||||
"00000000-0000-0000-0000-000000000000",
|
||||
"ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
}
|
||||
for _, uid := range tests {
|
||||
t.Run(string(uid), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name := NameFromUID(uid)
|
||||
if !nameRegexp.MatchString(name) {
|
||||
t.Errorf("NameFromUID(%q) = %q, does not match %s", uid, name, nameRegexp)
|
||||
}
|
||||
if len(name) > 63 {
|
||||
t.Errorf("NameFromUID(%q) = %q, length %d exceeds GCP's 63-char limit", uid, name, len(name))
|
||||
}
|
||||
if len(name) != len(namePrefix)+16 {
|
||||
t.Errorf("NameFromUID(%q) = %q, length %d, want %d", uid, name, len(name), len(namePrefix)+16)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameFromUID_distinctAcrossFleet(t *testing.T) {
|
||||
t.Parallel()
|
||||
const n = 10000
|
||||
seen := make(map[string]types.UID, n)
|
||||
for i := range n {
|
||||
uid := types.UID(fmt.Sprintf("00000000-0000-0000-0000-%012d", i))
|
||||
name := NameFromUID(uid)
|
||||
if prior, ok := seen[name]; ok {
|
||||
t.Fatalf("collision: NameFromUID(%q) == NameFromUID(%q) == %q", uid, prior, name)
|
||||
}
|
||||
seen[name] = uid
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user