You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Namespace deletion leaves AI Gateway keys and AI providers in Coder #353
When a namespace is deleted, Kubernetes deletes the CoderControlPlane and its CoderAIGatewayKey objects at the same time. The operator then cannot reach Coder: the control plane is gone, or its Coder pod is already down when the bounded pass runs. So it releases the finalizers without deleting the keys, and full-trust gateway keys stay valid in Coder. CoderAIProvider has the same gap for providers.
The key row stayed in Coder's ai_gateway_keys table.
The operator logged Releasing the finalizer without deleting the keys in Coder with reason ControlPlaneGone and the key ID.
Today the how-to (docs/how-to/manage-ai-gateway-keys.md) warns about this and gives the cleanup: delete the key objects before the namespace, or delete the logged keys with coder ai-gateway keys delete.
Options
A cluster-wide sweeper (plan risk RK7) that deletes ck8s-<uid>- keys whose UID no CoderAIGatewayKey has, after the settle window. The same idea works for providers.
Hold the control plane's deletion (a finalizer or a deletion check) while dependent CoderAIGatewayKey and CoderAIProvider objects exist, so they are cleaned up first.
Related items to decide together
While the control plane is being deleted, a failed operator-access read releases at once without a retry (CoderAIGatewayKey CRD with automatic rotation #156 plan, K9 notes item q). This is a second path that leaves keys.
The CoderAnswerMismatch gate is lost when the status write right after the mismatch fails (K9 notes item u). The settle window bounds this to one extra create, whose key the sweep deletes.
A metric or a Kubernetes Event for ControlPlaneGone releases. Today the log line is the only signal.
Maintainer decision: this must land before the next release tag (v0.1.5). It does not block merging the CoderAIGatewayKey activation (#346) to main, because no released version runs that controller yet, and the how-to documents the cleanup.
CoderAIProvider already shipped in v0.1.4 with the same gap. The v0.1.4 notes say to delete CoderAIProvider objects before their control plane, and that a provider deleted after its control plane stays in Coder. So the provider half is a fix for released behavior, and should go first if the work is split.
Prefer option 2 (hold the control plane's deletion while dependent objects exist) or a combination, and include items q and u and the metric or Event in the decision.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium
Follow-ups from the Codex review of #362 (part 1, the control-plane hold). I deferred both at first: each one only delays a control-plane deletion by at most the 10-minute hold, and neither leaks more than the code before #362 did. Owner: me, in the next #353 part.
Queue backlog at the hold deadline (🤖 fix: hold a deleted CoderControlPlane while AI Gateway keys and providers still use it #362 (comment)). The grace period counts only this control plane's dependents, but one worker per kind also serves unrelated objects. A backlog of slow reconciles can use up the grace, and the dependent then releases without a delete (the metric counts it). Fix: an explicit cleanup-done acknowledgement, or the cleanup records of the next part.
Foreground deletion seen after its marker is gone (🤖 fix: hold a deleted CoderControlPlane while AI Gateway keys and providers still use it #362 (comment)). When the operator is down during a foreground delete, the foregroundDeletion finalizer can be gone before the first reconcile, so the hold starts although Coder is gone. Dependents then retry for up to the hold plus the grace. Fix: also check that the owned Deployment exists before holding.
ID-less record dropped after a deployment switch (🤖 feat: write a cleanup record before each CoderAIProvider create (#353 part 2a) #374 (comment)). A create in deployment A loses its answer, the control plane then reaches deployment B, and B has a provider with the same name. ensureAIProviderRecord skips the deployment check because both IDs are empty, recordAIProvider clears the marker (NameConflict), and a later delete removes the record. The provider in A can leak. Fix: when CreateAttemptTime is set, check the record's deployment ID against buildinfo before the ID shortcut, and test this transition.
Key record keeps an older create time after a status edit (advisor note on 🤖 feat: write a cleanup record before each CoderAIGatewayKey create (#353 part 2c) #385, low). When someone clears status.lastCreateAttemptTime by hand, the observe step stamps it with the current time (fail closed), but recordAIGatewayKeyID rewrites the record only when the key ID changed. So the sweep can count its settle window from the older time. Needs a manual status edit. Fix: also rewrite the record when its create time is older than lastCreateAttemptTime.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Maintainer decision for part 2 (cleanup records): option B′, from an independent design review.
One ConfigMap per record, named ck8s-cleanup-<object UID>, in the operator's own namespace (POD_NAMESPACE), matched by Coder's deployment ID (/api/v2/buildinfodeployment_id, which lives in the Coder database), never by URL or by user-chosen name.
Why not the alternatives: a finalizer or hold cannot keep coderd running during a namespace delete; key expiry does not exist in Coder v2.37.3; a sweeper without records would guess from absence and could delete another cluster's keys; a cluster-scoped CRD (option A) gives little extra safety for a new public API type.
Invariants the implementation must enforce:
Write the record before the create call in Coder; if the write fails, do not create.
A record holds no secrets: kind, object UID, namespace/name, deployment ID, key name prefix or provider nonce, Coder IDs once known, created-at.
Read the deployment ID from Coder in the same reconcile as the create (or from control plane status written by the operator). Never from user input.
The sweeper acts only through a control plane whose current deployment ID equals the record's.
Delete only exact recorded IDs, a key with the record's ck8s-<uid>- prefix, or a provider whose display name is the record's nonce. Never by user-chosen name alone.
Act only when the object with that UID no longer exists (lookup + UID compare) and the 5-minute settle window has passed.
Remove the record only after Coder confirms the delete (success or Coder's own 404) in the matching deployment.
A record no matching deployment reaches stays, with a metric and an Event. An admin can delete it after deleting the key or provider by hand.
Only the operator service account can write these ConfigMaps (namespaced Role). No user-facing role gets write access.
Ship the provider half first (fixes released v0.1.4 behavior).
Keep the docs warning: it is the only fix when no control plane ever reconnects to that database.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium
🤖 fix: hold a deleted CoderControlPlane while CoderMCPServer objects still use it #384 bounds the MCP delete pass by the hold end and counts its retry from the end of the pass. The provider and key delete paths (coderaiprovider_delete.go near lines 84-88, coderaigatewaykey_delete.go near lines 56-60) still count the retry from the start of the pass and have no hold-end deadline. A slow pass there can run into the grace period.
controlPlaneDependents has no unit test for the per-kind count when CoderMCPServer is the largest kind.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium
Activity
ThomasK33 commentedon Oct 14, 2026
Maintainer decision: this must land before the next release tag (v0.1.5). It does not block merging the
CoderAIGatewayKeyactivation (#346) tomain, because no released version runs that controller yet, and the how-to documents the cleanup.CoderAIProvideralready shipped in v0.1.4 with the same gap. The v0.1.4 notes say to deleteCoderAIProviderobjects before their control plane, and that a provider deleted after its control plane stays in Coder. So the provider half is a fix for released behavior, and should go first if the work is split.Prefer option 2 (hold the control plane's deletion while dependent objects exist) or a combination, and include items q and u and the metric or Event in the decision.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:mediumThomasK33 commentedon Oct 14, 2026
Follow-ups from the Codex review of #362 (part 1, the control-plane hold). I deferred both at first: each one only delays a control-plane deletion by at most the 10-minute hold, and neither leaks more than the code before #362 did. Owner: me, in the next #353 part.
OperatorAccessReadyis false at deletion time, the deletion path never runsreconcileOperatorAccessagain. Dependents wait withOperatorAccessNotReadyfor 10 minutes and then release without a delete. Before 🤖 fix: hold a deleted CoderControlPlane while AI Gateway keys and providers still use it #362 they released at once with the same leak. Fix: skip the hold when operator access is not ready, or keep repairing it during the hold.A terminating dependent with another finalizer keeps the hold(🤖 fix: hold a deleted CoderControlPlane while AI Gateway keys and providers still use it #362 (comment)). After its cleanup finalizer goes, an object that has a second finalizer still counts as a dependent until the 10 minutes pass. Fix: a terminating dependent holds only while it still has its AI cleanup finalizer.foregroundDeletionfinalizer can be gone before the first reconcile, so the hold starts although Coder is gone. Dependents then retry for up to the hold plus the grace. Fix: also check that the owned Deployment exists before holding.coderaiprovider_delete.gousestime.Now()for the hold check, while the key path usesr.Clock.Now(). Fix: use the reconciler clock, so tests can drive the provider hold end./buildinfocheck and the DELETE can reach pods on different databases, so a 404 from the new one can remove the record while the provider stays in the old one. Fix: recreate the Deployment on a database change, or check the deployment again after the delete. The same window applies to the sweep in 🤖 feat: sweep the cleanup records of CoderAIProvider objects that went without a delete (#353 part 2b) #375 (cleanup_sweep.go): its buildinfo match and its DELETE are also two requests.ensureAIProviderRecordskips the deployment check because both IDs are empty,recordAIProviderclears the marker (NameConflict), and a later delete removes the record. The provider in A can leak. Fix: whenCreateAttemptTimeis set, check the record's deployment ID against buildinfo before the ID shortcut, and test this transition.status.lastCreateAttemptTimeby hand, the observe step stamps it with the current time (fail closed), butrecordAIGatewayKeyIDrewrites the record only when the key ID changed. So the sweep can count its settle window from the older time. Needs a manual status edit. Fix: also rewrite the record when its create time is older thanlastCreateAttemptTime.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 14, 2026
Maintainer decision for part 2 (cleanup records): option B′, from an independent design review.
One ConfigMap per record, named
ck8s-cleanup-<object UID>, in the operator's own namespace (POD_NAMESPACE), matched by Coder's deployment ID (/api/v2/buildinfodeployment_id, which lives in the Coder database), never by URL or by user-chosen name.Why not the alternatives: a finalizer or hold cannot keep coderd running during a namespace delete; key expiry does not exist in Coder v2.37.3; a sweeper without records would guess from absence and could delete another cluster's keys; a cluster-scoped CRD (option A) gives little extra safety for a new public API type.
Invariants the implementation must enforce:
ck8s-<uid>-prefix, or a provider whose display name is the record's nonce. Never by user-chosen name alone.Keep the docs warning: it is the only fix when no control plane ever reconnects to that database.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:mediumfeat: sweep the cleanup records of CoderAIProvider objects that went …
ThomasK33 commentedon Oct 14, 2026
Follow-ups from #384 (control plane hold now covers
CoderMCPServer, fixes #382). None of them blocks v0.1.5.docs/how-to/manage-ai-providers.mdnames only providers and keys in its hold sentence. Add MCP servers.coder_k8s_cleanup_released_without_delete_totaldoes not count aCoderMCPServerthat releases its finalizer without a delete in Coder.CoderAIProvider.coderaiprovider_delete.gonear lines 84-88,coderaigatewaykey_delete.gonear lines 56-60) still count the retry from the start of the pass and have no hold-end deadline. A slow pass there can run into the grace period.controlPlaneDependentshas no unit test for the per-kind count whenCoderMCPServeris the largest kind.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:medium