# terdut-operator build roadmap This is the staging plan for implementing the operator against `DESIGN.md`'s settled decisions. It exists for the same reason `DESIGN.md` and `SERVICE-ACCOUNTS.md` do: so each stage starts from an agreed sequencing instead of re-litigating "what do we build first" mid-PR. ## Sequencing call this roadmap makes **The operator creates and owns every `TerdutServer` it manages — it never adopts one deployed independently, by hand or by `charts/terdut-server`** (`DESIGN.md` §1). An earlier version of this roadmap staged `TerdutServer`'s Deployment/Service/bootstrap takeover separately (old Stage 5), behind a hand-deployed server the simpler CRDs could be proven against first. That staging existed only because a credential-less operator couldn't `/api/bootstrap` its way into a server something else had already bootstrapped (`DESIGN.md` §6's original gap). With no server to adopt at all, that split has nothing left to justify it: `TerdutServer` now builds its full lifecycle — Deployment, Service, database wiring, bootstrap, credentials — in one stage, Stage 1, since bootstrap only has something to bootstrap once the Deployment exists. ## Stage 0 — Scaffolding & CI - `go.mod` (`git.ryuvia.com/niklas/terdut-operator`) + Kubebuilder v4 scaffold (`cmd/main.go`, `config/`, `Makefile`, `PROJECT`), matching terdut-server's Go toolchain and house style (§3). Kubebuilder's own scaffolded `Makefile` already wires `manifests`/`generate` (`controller-gen`) and `setup-envtest` into `test`, and `golangci-lint` into `lint`, all fetched on demand into `bin/` — no separate install step needed beyond what `make test`/`make lint` already do. - `.release.conf` deliberately **not** added yet: it names a `HELM_CHART` this repo doesn't have until Stage 5. Adding it now would either be a stub that lies about what's releasable or dead config nobody can run — it lands in Stage 5, alongside the chart it describes. - Gitea Actions CI calling `fmt lint test`, mirroring terdut-server's `ci.yaml` convention (its `CLAUDE.md`: "a green gate here and a green pipeline are the same code, not two descriptions of it") minus the `chart`/`security` jobs, which need a chart (Stage 5) and real controller code (Stage 1+) respectively to have anything to check. - Drop kubebuilder's default `.github/workflows/*` scaffold — this org runs on Gitea, not GitHub; `.gitea/workflows/ci.yaml` is the only CI this repo has. - Housekeeping: drop the stray `.DESIGN.md.swp` (leftover vim swapfile, shouldn't be committed); correct `DESIGN.md` §6/§13's "v1-blocking, not v1-shippable" language — the service-account feature it was blocking on has since shipped in terdut-server. **Done when:** CI is green on an otherwise-empty scaffold. ## Stage 1 — `TerdutServer`, full lifecycle Supersedes the Stage 1 shipped before this redesign (commit `1be7cf2`) outright — that `TerdutServerSpec`/`Status`/controller/tests implemented the now-removed bring-your-own path and get replaced wholesale, not extended. New commits build forward over the old ones; no git history rewrite. - Full §4.1 spec: `image`, `replicas`, `networking`, `database`, `sweeper`, `deadman`, `notify`, `oidc`, `passwordLogin`, `allowedTeams`, all together — no narrowing, since bootstrap needs the Deployment it's narrowed away from in the version this replaces. - Controller manages the Deployment + Service, both Postgres paths from §8 at once (bring-your-own DSN *and* the Zalando `postgres-operator` `postgresClusterRef` integration — not sequenced, per the user's call), and bootstrap/credentials per §6's self-registration flow: `/api/bootstrap` once the Deployment has a ready replica, checkpoint the admin key, mint the instance-scoped service account, generated credentials Secret in the operator's own namespace, `status.credentialsSecretRef`. Finalizer cleans up that Secret (and the checkpoint, if one's still there) on delete — there's no server-side row to clean up alongside it: terdut-server's API has no way to delete a user or a service account, only to revoke individual keys, so there's nothing to undo there regardless. - RBAC: read-only watch on `postgresql.acid.zalan.do`, degrading gracefully if that CRD isn't installed (§8, §9). - Shipped, scoped down from §8's full ambition in two ways, both called out in code rather than silently dropped: no live watch on the Zalando- generated credentials Secret for rotation (relies on the periodic resync to notice eventually, higher latency than a watch); no Gateway API `HTTPRoute` creation from `spec.networking.hostname`/`gatewayListener` (needs the Gateway API types as a new dependency, and nothing about proving a `TerdutServer` boots and bootstraps a real server depends on external ingress existing). Both are near-term follow-ups, not deferred to a later stage. - `envtest` covering Deployment/Service reconciliation and both database paths — the Zalando path needs that CRD's schema vendored into the test environment (there's no real `postgres-operator` controller in `envtest`, only the CRD shape to create fixture objects against) — plus the self-registration flow against an `httptest.Server` fake of `/api/bootstrap` and `/api/service-accounts` (§11), including the adopt-on-409 recovery path and the one fail-closed case (`BootstrapStateLost`), not just the happy path. - **Done, 2026-10-01**: a real `kind` end-to-end pass (bring-your-own DSN, real terdut-server `v0.33.0` image, operator built into a real image and deployed as a real Pod, not `go run` against the cluster). `TerdutServer` went `Ready`; the generated credential authenticated and exercised its real capability against the actual server (`GET`/`POST /api/teams` → `200`/`201`, confirmed from terdut-server's own access log). Caught two real bugs no `envtest` suite could have (its client bypasses RBAC): `.dockerignore`'s `!**/*.go` not working under podman, and missing RBAC for `events.k8s.io` (the new events API `GetEventRecorder` uses) — both fixed. The Zalando path can additionally be validated for real against the org's own cluster later, where `postgres-operator` already runs, rather than only in a disposable `kind` stand-in — not done in this pass. ## Stage 2 — `TerdutTeam` - §4.2: `serverRef` resolution, real update-in-place (POST create / PUT rename / PUT oidc-groups), team-scoped service-account minting once `Ready` (§6 point 3), finalizer that DELETEs the team server-side and its credential Secret. - First place the "every child resolves its own `teamRef` → `TerdutTeam.status`, never chains up to `TerdutServer`" pattern (§5) gets proven end to end. ## Stage 3 — `TerdutEscalationRule` + `TerdutDeadmanSwitch` - Built together: both stay same-namespace-as-their-`TerdutTeam` (§1), so neither exercises cross-namespace complexity, but together they cover the two different reconciliation shapes §5's table calls out — whole-policy PUT-on-drift for the escalation policy, delete-and-recreate (no PUT available) for the dead man's switch — against the same shared create/finalizer/resync scaffolding Stage 2 already built. ## Stage 4 — `TerdutAlertSource` - Last of the children on purpose: it has the subtlest failure mode of the four. Covers webhook Secret generation/ownership (§4.5, §7), the `WebhookSecretLost` fail-closed condition + `Warning` event (§5, added 2026-09-30), and the kind-change delete-and-recreate rotation path — all easier to get right with the other three controllers' patterns already in place to build on. ## Stage 5 — Installer chart + real release - Package CRDs + the operator's own Deployment/RBAC into the installer chart §10 describes; wire `.release.conf`/release-vars the same way terdut-server does; run it through the `release` skill for a real first cut. - Full `kind` end-to-end test per §11: create `TerdutServer` → `TerdutTeam` → one of each child kind → verify via terdut-server's own API that each object exists with the right shape → delete the CR → verify the server-side object is gone. ## Deferred (§13, unchanged by this roadmap) Cross-namespace `allowedTeams` exercised against a real second namespace, CloudNativePG support, narrower-than-namespace Secret RBAC for the webhook Secret, gitops-managed team membership, automatic Deployment restart on upstream Postgres credential rotation, admission webhooks/CEL-only validation limits, OLM packaging.