paths, self-registration bootstrap)
Replaces the bring-your-own-only Stage 1 (commit 1be7cf2) wholesale, per
the redesign in the previous two commits: the operator creates every
server it manages, so self-registration (DESIGN.md §6) is the only
bootstrap path, and Deployment/Service/database management builds
together with it (ROADMAP.md Stage 1) rather than behind a separate
later stage.
Grounded in terdut-server's actual chart (charts/terdut-server/templates/
deployment.yaml, values.yaml), not reconstructed from DESIGN.md's
illustrative YAML alone -- env var names, the password-via-PGPASSWORD
convention, the Recreate deployment strategy, /healthz probes, and the
TERDUT_OPERATOR_MODE=true decision (always on here, unlike the chart's
default-off: every write this operator's own future controllers make
goes through a service account already) all match that source exactly.
- api/v1alpha1: full TerdutServerSpec (image, replicas, networking,
database, sweeper, deadman, notify, oidc, passwordLogin, allowedTeams).
spec.database is a oneOf (dsn xor postgresClusterRef) via CEL
XValidation. No spec.credentialsSecretRef -- removed entirely in the
prior redesign commit, not carried forward.
- internal/controller:
- terdutserver_deployment.go: Deployment + Service via CreateOrUpdate,
owned (OwnerReference), env built field-for-field against the chart.
- terdutserver_database.go: both §8 paths. The Zalando path resolves
the postgresql.acid.zalan.do CR by convention (database/role both
"terdut", matching every DESIGN.md example) and only ever confirms
its generated credentials Secret exists -- never reads the value,
same "wire a secretKeyRef, don't read it" posture the DSN path takes.
classifyClusterGetError is its own function specifically so the
CRD-not-installed case (meta.IsNoMatchError) is unit-testable without
a real client.
- terdutserver_bootstrap.go: self-registration, checkpointed against
both real crash windows (DESIGN.md §6 point 1) -- an admin-key
checkpoint Secret, and adopt-via-GET+mint-new-key on a 409 from
creating the service account. BootstrapStateLost is its own error
type so Reconcile can route it to a condition instead of an infinite
retry.
- terdutserver_controller.go: ties it together -- finalizer add, DB
resolution, Deployment/Service reconcile, wait for a ready replica,
bootstrap, Ready/Bootstrapped/DatabaseReady conditions. Finalizer on
delete only removes the generated Secrets: terdut-server's API can't
delete a user or service account, only revoke keys, so there's
nothing server-side to undo.
- internal/tdclient: added Bootstrap, CreateInstanceServiceAccount,
GetServiceAccountByName, CreateServiceAccountKey, matching
terdut-server's real handlers' request/response shapes (internal/api/
users.go, service_accounts.go in that repo) field-for-field.
- Tests: envtest suite covering the full DSN-path lifecycle end to end
(finalizer -> Deployment/Service -> simulated readiness -> real
bootstrap against an httptest.Server fake), the adopt-on-409 recovery
path, BootstrapStateLost, both Zalando outcomes (cluster not found;
cluster + Secret found -> real DSN -> Ready), and deletion. A minimal
test-only stub of the Zalando CRD (internal/controller/testdata) lets
envtest create fixture objects without a real postgres-operator
installed. 74.0%/44.7% coverage, 0 lint issues.
- Two things scoped down from §8's full ambition, called out in code and
ROADMAP.md rather than silently dropped: no live watch on the
Zalando-generated Secret for rotation (periodic resync notices
eventually, not immediately), no Gateway API HTTPRoute creation from
spec.networking (would add a new dependency; nothing about proving
bootstrap works depends on external ingress existing). Both are
near-term follow-ups.
Verified locally: make fmt lint test build all clean.
Implements the narrowed Stage 1 scope from ROADMAP.md, against the
bootstrap-flow fix from DESIGN.md §4.1/§6 (the earlier self-registration
flow couldn't work unauthenticated against terdut-server's real
AuthMiddleware -- see that commit for the full trace).
- api/v1alpha1: TerdutServer with spec.endpoint + spec.credentialsSecretRef
+ spec.allowedTeams (image/replicas/networking/database deferred to
Stage 5, per DESIGN.md's own narrowing). SecretKeyRef has no namespace
field -- always the operator's own, by construction.
- internal/controller: TerdutServerReconciler implements exactly the
bring-your-own path -- adopt spec.credentialsSecretRef if the Secret
exists and has data under the given key, probe GET /api/version as a
reachability check, set Ready/Bootstrapped conditions accordingly.
Self-registration (the /api/bootstrap race) is not implemented; unset
spec.credentialsSecretRef reports Ready: False, reason:
CredentialsSecretRefRequired, not an attempt at a flow that would fail
unauthenticated anyway. No finalizer: this stage creates nothing
server-side and adopts rather than generates its Secret, so there's
nothing to clean up on delete yet.
- internal/tdclient: minimal terdut-server API client (Version only, the
one call this stage needs), styled after terdut-tui's own
internal/api/client.go per terdut/CLAUDE.md's mirroring convention.
- Tests: envtest suite covering all four not-ready paths plus the happy
path (fake terdut-server via httptest.Server, per DESIGN.md §11), and a
focused unit suite for tdclient. 75.6%/82.4% coverage.
- Event recording uses the new events.k8s.io/v1 recorder API
(mgr.GetEventRecorder), not the deprecated GetEventRecorderFor --
caught by golangci-lint's staticcheck before it shipped.
Verified locally: make fmt lint test build all clean, 0 lint issues, all
specs pass.