# 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 `TerdutServer` does **not** take over Deployment/Service/bootstrap (§4.1, §10) until Stage 5. Every earlier stage targets a terdut-server deployed by hand, via the existing `charts/terdut-server` chart, into a namespace dedicated to this work — disposable, no risk to anything else, and explicitly not something this roadmap plans to migrate in place. This is a green-field build: there's no existing install this operator is on the hook to preserve, so the hand-deployed instance from Stage 1 is meant to be retired once Stage 5's operator-managed instance is proven, not migrated. `TerdutTeam.spec.serverRef` still needs a real `TerdutServer` object to resolve against from Stage 1 onward, though (§4.2, §6) — so `TerdutServer` shows up early, just with a deliberately narrow first spec covering only what §6's bootstrap/credential flow needs. `image`/`replicas`/`database` land in Stage 5, not before. ## 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 6. Adding it now would either be a stub that lies about what's releasable or dead config nobody can run — it lands in Stage 6, 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 6) 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`, bootstrap/credentials only - Deploy terdut-server via its existing chart into a fresh, disposable namespace — manual, out-of-band, nothing operator-managed yet. This chart run bootstraps the server itself, before the `TerdutServer` CR exists. - `TerdutServer` CRD narrowed to `spec.endpoint` + `spec.credentialsSecretRef` + `spec.allowedTeams`; the rest of §4.1's spec (`image`, `replicas`, `networking`, `database`) waits for Stage 5. - Controller implements §6's bring-your-own path only: `spec.credentialsSecretRef` set and the Secret exists → adopt it, `status.credentialsSecretRef` mirrors it, `Bootstrapped`/`Ready: True`. Unset (or not found yet) → `Ready: False`, requeue, no API call made. **Self-registration (the `/api/bootstrap`-race fallback in §6 point 1) is explicitly deferred to Stage 5**, not implemented here: Stage 1's own setup never exercises it (the chart always bootstraps first, per above), and this narrowed spec has no username/email fields for it to call `/api/bootstrap` with in the first place. Adding it later is additive, not a breaking change to this stage's shape. - No Deployment/Service reconciliation at all in this stage. - First `envtest` suite (controller-runtime's fake API server) plus an `httptest.Server` fake of terdut-server's bootstrap/service-account endpoints, per §11. ## 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 — `TerdutServer` absorbs Deployment/Service/bootstrap (§10, executed) - Extend `TerdutServer` to the full §4.1 spec and stand up a *second*, operator-managed terdut-server in the same dev namespace — not an in-place takeover of Stage 1's hand-deployed instance. Once this is solid, that hand-deployed instance is simply retired. - Postgres integration (§8): bring-your-own DSN path first (no external CRD dependency); Zalando `postgres-operator` path as a follow-up — split into its own stage if it turns out bigger than expected once started. - This is where §10's "the chart becomes an installer chart" recommendation actually gets executed, rather than just recommended. ## Stage 6 — 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.