b0a431f2a4294b30f1a967c8960f36f6240ceebe
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
62664c93ff |
Keep the instance credential across a TerdutServer delete, and adopt it on recreate
Deleting a TerdutServer removed the credential Secrets but never touched the database, so a recreated one found a server that was already bootstrapped and no key for it: /api/bootstrap answered 403 and the operator stopped at BootstrapStateLost, whose message and DESIGN.md both said "delete and recreate". That is how the terdut-demo install on the cluster got stuck on 2026-10-03: Helm's cleanupOnFail deleted its TerdutServer after a failed upgrade, the recreate found the bootstrapped database, and it sat at Ready: False for five days until the database was reset by hand. Recreating cannot fix it, because the finalizer clears Secrets and the database is not its to reset, so "a fresh create starts clean" was only ever true when the database went with it. spec.credentials.deletionPolicy is Retain by default: the finalizer keeps the instance credential Secret (Delete removes it, as before). The bootstrap checkpoint is always removed. Before calling /api/bootstrap, reconcile now looks for the retained Secret and asks the server for the operator's own service account with its token. Accepted: adopt it and skip bootstrap. Rejected with 401/403: the Secret outlived a database reset, so ignore it and bootstrap like a first install, which replaces it. Any other error retries. terdut-server's own tests already call that endpoint with an instance-scoped key, so the permission is not new. BootstrapStateLost is still the answer when the server is bootstrapped and no credential it accepts survives, but its message now names the Secret to restore and says that recreating does not clear the database. DESIGN.md §6 says the same, and the chart passes the setting through as terdutServer.credentials.deletionPolicy. A retained Secret of a TerdutServer that is gone for good is an orphan to delete by hand. It is inert: nothing adopts it unless the server accepts the token. Checked on the kind demo with a locally built image against the real terdut-server v0.43.0: deleting the TerdutServer kept the Secret, recreating it reached Ready with the same credential (identical hash) and both TerdutTeams came back Ready with their original ids. The controller specs cover adoption, a rejected token after a reset, the bootstrapped-and-rejected failure, and both deletion policies. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6a699d4341 |
Let TerdutServer customize its pod, and never manage its own ingress
spec.pod (api/v1alpha1/terdutserver_types.go): annotations, nodeSelector,
tolerations, affinity, topologySpreadConstraints, resources, pod and
container securityContext, serviceAccountName, extraEnv/extraEnvFrom,
extraVolumes/extraVolumeMounts, imagePullSecrets, and an optional
disruptionBudget. All direct corev1 passthrough -- no wrapper types buy
anything for any of these, matching how CloudNativePG and the Zalando
postgres-operator both expose the same knobs, and matching this repo's
own SweeperSpec precedent ("wrap only when a round-trip through a
different type buys something"). affinity is pure user-supplied
passthrough, not a toggle-plus-generated-default the way a multi-replica
cluster operator's pod anti-affinity usually is: this operator never
auto-generates one, since spec.replicas above 1 isn't a supported
topology (the sweeper/notifier singleton constraint). Considered and
declined for this round: priorityClassName, pod labels beyond
annotations, and a HorizontalPodAutoscaler -- the last of those would
directly contradict the singleton constraint above.
disruptionBudget is the one field here that isn't a plain PodTemplateSpec
knob: when set, the controller now reconciles a PodDisruptionBudget
selecting the TerdutServer's own pods (new terdutserver_pdb.go); clearing
it deletes any it previously created. New RBAC marker on
poddisruptionbudgets to match.
Driven by a public-release pass: looking past this project's own use case
at what a mature, general-purpose operator CRD exposes here (researched
against Zalando postgres-operator and CloudNativePG specifically), not
just the fields this install happened to need.
Separately, and found while answering a question about exposing
TerdutServer through Istio instead of Gateway API: spec.networking's own
doc comment quietly promised a Gateway API HTTPRoute this operator would
build eventually ("a near-term follow-up, not deferred"). That promise is
wrong for a public release -- an operator managing someone's ingress
mechanism for them is a worse default than not touching it at all, and a
surprise HTTPRoute appearing once that follow-up eventually landed would
have been exactly backwards for an Istio (or plain-Ingress, or
intentionally-unexposed) install. Made the non-goal explicit and
permanent instead (DESIGN.md §1), removed the dead `gatewayListener`
field it was the only consumer of (zero runtime call sites anywhere --
setting it already had no effect, so this is a schema cleanup, not a
behavior change), and corrected ROADMAP.md's framing. hostname/servicePort
stay: both are live (TERDUT_PUBLIC_URL, container/Service port), this
operator just never acts on hostname for exposure. Added
examples/networking (Gateway API HTTPRoute, Istio VirtualService) showing
how to expose the plain ClusterIP Service the operator already creates --
outside the operator itself, as illustrations, not as something
examples/demo applies automatically.
No new terdut-server version requirement: both changes are CRD/controller-
only, nothing about the API this operator's bootstrap flow depends on
changed.
|
||
|
|
1c45b7e80b |
Stage 1: fix two real bugs the kind e2e pass caught, neither envtest could
CI / test (push) Has been cancelled
Ran a full kind end-to-end pass per ROADMAP.md's open item: real kind cluster, real disposable Postgres, the real terdut-server v0.33.0 image, the operator built into a real image and deployed as a real Pod (not `go run` against the cluster -- that was tried first and correctly failed on cluster DNS not resolving from outside the cluster network, which is expected, not a bug). Result: TerdutServer went Ready, the generated credentials Secret held a real tdsa_-prefixed service-account key, and that key successfully authenticated and exercised its real intended capability against the actual server (GET/POST /api/teams -> 200/201) -- confirmed from terdut-server's own access log, not just our side. Stage 1's actual goal (ROADMAP.md) is proven, not just asserted. Two real bugs surfaced that no envtest suite could have caught, since envtest's client bypasses RBAC entirely: - .dockerignore's `!**/*.go` doesn't work under podman (the scaffold's own comment already named this exact gotcha, buildah/containers#6417, and pointed at the fix) -- `docker build` was silently building from an empty source tree ("package cmd/main.go is not in std") until this was pinned down. Fixed by re-including cmd/api/internal by name, as that comment suggested doing if this happened. - The controller had no RBAC for events.k8s.io (the new events API GetEventRecorder uses, unlike the deprecated GetEventRecorderFor) -- every Event emission failed server-side ("Server rejected event (will not retry!)"), silently, since event-recording failure doesn't fail reconciliation. Reconciliation itself was never affected, but DESIGN.md §12's observability goal (every externally-visible action emits an Event) silently wasn't being met in any real deployment. Added +kubebuilder:rbac for events.k8s.io/events (create, patch); confirmed fixed by restarting the operator and checking `kubectl describe terdutserver` actually shows the Event afterward, not just that the log line stopped. Also noted, not fixed here (a different repo's bug): terdut-server's own GET /api/me 500s for a service-account caller rather than a clean 4xx -- that endpoint assumes a human user in context. Worth a terdut-server issue, not an operator concern. |
||
|
|
8064876cb1 |
Stage 1: TerdutServer full lifecycle (Deployment, Service, both database
CI / test (push) Successful in 1m46s
paths, self-registration bootstrap)
Replaces the bring-your-own-only Stage 1 (commit
|
||
|
|
1be7cf2b7f |
Stage 1: TerdutServer, bring-your-own bootstrap credentials
CI / test (push) Successful in 1m41s
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. |