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:
2026-08-07 22:44:22 +02:00
parent f28766fce3
commit a4a483acbc
11 changed files with 859 additions and 2 deletions

View File

@@ -6,7 +6,7 @@ Pairs with [docs/plans/2026-08-07-1747-proxy-operator.md](../plans/2026-08-07-17
- [x] Step 0 — Branch and scaffold
- [x] Step 1 — API types (`api/v1alpha1/proxy_types.go`)
- [ ] Step 2 — Provider contract (`internal/provider/`)
- [x] Step 2 — Provider contract (`internal/provider/`)
- [ ] Step 3 — Mock provider (`internal/provider/mock/`)
- [ ] Step 4 — Reconciler (`internal/controller/`)
- [ ] Step 5 — Health engine (`internal/health/`)
@@ -148,3 +148,54 @@ which only `make test` does via `setup-envtest`. Plain `go test ./...` fails the
`internal/controller` package with a `/usr/local/kubebuilder/bin/etcd: no such file`
error that has nothing to do with the code. Use `make test`, not `go test ./...`,
whenever the controller package is in scope.
## Step 2 — Provider contract (`internal/provider/`)
Wrote the `Provider` interface (`Create`/`Get`/`Delete`/`ListByTag`), `Instance`,
`Placement`, `CreateRequest`, and the GC-contract label constants
(`provider.go`); the error taxonomy with multi-error `Unwrap() []error` so
`errors.Is` and `errors.As` both work off the same wrapped value
(`errors.go`); deterministic instance naming via SHA-256 → base32 → 16
chars (`name.go`); and `--providers-config` YAML parsing with fail-fast
validation (`config.go`).
One deliberate deviation from the plan's file layout: the plan listed
`internal/provider/metrics.go` as part of this step, but the
`provider.WithMetrics` decorator it describes is Step 9's concern (it needs
the Prometheus vectors that don't exist until the metrics package is
built) and nothing in this step depends on it existing yet. Deferred to
Step 9 rather than writing a decorator with nowhere to register its
metrics.
The registry package (`internal/provider/registry/registry.go`) came out
slightly different from the plan's sketch, and better for it: instead of a
package-level `var builtin = map[string]Constructor{"mock": mock.New, "gcp":
gcp.New}` living inside the registry package, `Build` takes the
`map[string]Constructor` as a parameter. This means `registry` has zero
import on `internal/provider/mock` or `internal/provider/gcp` — neither of
which exists yet at this point in the plan (mock is Step 3, gcp is Step 8)
— so the package compiles today instead of only once both are done, and the
explicit wiring lives at the composition root (`cmd/main.go`, Step 10)
rather than being smeared into the registry package itself. Still fully
avoids the import-cycle trap the plan called out.
Ran the full suite:
```bash
go mod tidy # sigs.k8s.io/yaml (already an indirect dep of the k8s.io toolchain) promoted to direct
go build ./... && go vet ./...
go test -race -v ./internal/provider/...
make test
```
`internal/provider` landed at 96.2% coverage, `internal/provider/registry` at
100%. `make test` also ran `go fmt ./...`, which reformatted `errors.go`'s
struct-field comment alignment before its first commit — no logic change,
just gofmt on a brand-new file.
Worth noting: `sigs.k8s.io/yaml` (not `gopkg.in/yaml.v3`) was picked for
`--providers-config` parsing specifically because it has `UnmarshalStrict`
built in (rejects unknown fields, which is what "fail fast on unknown type"
in the plan actually needs) and was already pulled in transitively by the
k8s.io toolchain, so no new dependency was added — `go mod tidy` just
promoted it from indirect to direct.