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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user