Files
Jan Novak a4a483acbc 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>
2026-08-07 22:44:22 +02:00

62 lines
1.5 KiB
Go

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
}
}