Skip to content

🤖 feat: pool Kind phases P6 to P8, offline stubs for every phase, and deleting a released used member deletes its workspace (#153 PR 13) - #445

Merged
ThomasK33 merged 12 commits into
mainfrom
pool-e2e-p6-p8
Oct 31, 2026
Merged

ThomasK33 merged 12 commits into
mainfrom
pool-e2e-p6-p8

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary

Plan PR 13 of #153. The Kind E2E job now runs CoderWorkspacePool phases P6 to P8: use detection, scale-down, deletes, and a server dry run. hack/e2e-workspace-pool_test.sh now covers every phase P1 to P8 offline. A cluster admin who deletes a used member that the pool released now deletes its Coder workspace too.

Refs #153

Background

PR 12 (#441) activated the pool and ran Kind phases P1 to P5. Plan row 13 adds P6 to P8, offline stub tests for every phase, and the check of risk R1. #441 left one open discrepancy for this PR: a used member that a person deletes kept its workspace, but plan P8 expects the delete to remove it.

Decision: deleting a released used member deletes its workspace (option B)

The plan contradicts itself here:

  • 6.5 item 2 and threat T7 keep the workspace of any member without a claim that shows use ("a leaked workspace is better than lost work").
  • P8, decision D6 ("A person deletes it.") and 6.5 item 5 ("stays until a person deletes it") expect a person's delete to remove the workspace.

This PR follows P8. The last-use check exists for deletes that cannot see the use: a scale-down that races a usage flush, and garbage collection after a pool delete. A used member that the pool released has no controller owner, so garbage collection never deletes it. Users have read-only RBAC on members (T4). Only a cluster admin can delete it, which is a deliberate act, like deleting a claim. A namespace deletion is not such a delete. The namespace controller deletes members and the CoderControlPlane in no fixed order (an independent verifier showed that the member can come first). So before the delete build, a released used member reads its namespace without the cache, and if the namespace is being deleted it releases without a Coder call, as the control-plane-gone path does. A failed namespace read keeps the finalizer and returns the error. A used member that its pool still controls keeps today's rule, and so does a use that the member first sees at delete time.

Implementation

  • coderworkspacepoolmember_cleanup.go: the cleanup runs no last-use check for a member that is Used=True and has no controller owner reference. Its delete then works like a claim's delete, unless the member's namespace is being deleted. The operator reads the namespace without the cache and already has get on namespaces, so RBAC does not change. TestPoolMemberCleanupDeletesReleasedUsedMember failed before the fix (commit 916c67c) and passes after it. It also checks that two members keep their workspace: a used member that its pool still controls, and a member without a controller (for example after a pool delete with --cascade=orphan) whose use the delete finds first. Without the Used=True term in the new rule, the second case fails. TestPoolMemberCleanupKeepsReleasedWorkspaceInDeletedNamespace deletes the namespace with the member first and with the control plane first, and checks that a failed namespace read keeps the finalizer. It failed before the namespace check (commit d6c700a: member first and read failure red, control plane first green).
  • The usedReplicas field doc and the how-to now say this plainly. Deleting a released used member deletes its workspace in Coder. To keep the work, copy it out first. A used member that still lists the pool as its controller is not released yet, and deleting it keeps the workspace. Deleting the namespace keeps a used member's workspace. I found no other way to keep it that the code supports: a removed finalizer comes back while the member is not being deleted.
  • hack/e2e-workspace-pool.sh (plan 10.4):
    • P6: POST /api/v2/workspaces/{id}/usage with an empty body for one unclaimed member. Within 3 minutes the member must be Used=True and released, the pool must build back to 4 ready unclaimed members, and usedReplicas must be 1.
    • P7: scale to 1. An include_deleted read must show a succeeded delete build for each of the 3 extra unclaimed workspaces. The kept, the bound and the used workspace must stay live.
    • P8: deleting the claim deletes its workspace. Deleting the pool deletes the unclaimed workspace and keeps the used member and its workspace. Deleting the used member by hand deletes its workspace. kubectl apply --dry-run=server of the pool must create no pool, no member and no workspace within 10 s.
    • The final cleanup only restores spec.workspacePools, because P8 already deleted every object.
  • hack/e2e-workspace-pool_test.sh: kubectl and curl stubs over one fake cluster and Coder, with a fake pool controller that runs one pass per stub call. It has 16 scenarios:
    • The pass scenario runs P1 to P8 and must leave no object and no live workspace behind.
    • 15 negative controls each break one behavior, and the driver must fail with the check that covers it: no token ref, an OIDC owner, members before the allowlist, a zero deadline, an accepted --replicas=101, a bind that builds, a duplicate create after the restart, an undetected use, a false use, a scale-down that keeps workspaces, a claim delete and a pool delete that keep their workspaces, a pool delete that takes the used member, a used-member delete that keeps the workspace, and a dry run that creates the pool.
    • Every scenario fails when the operator token, the owner session or the owner password shows up outside its ::add-mask:: line, in argv, or in a file left in the work dir. It also fails when the port-forward keeps running.
  • CI: the pool step name and comment now say P1 to P8.

Validation

  • Local gates passed: make verify-vendor, make test, make build, make lint, make test-integration, make docs-check (mkdocs strict through nix develop), markdownlint, cspell, actionlint, and make test-scripts (642 s, every lifecycle scenario and all pool scenarios).

  • Stub harness controls (scratch copies of the driver):

    • The driver from main (P1 to P5 only) fails the pass scenario ("phases P1 to P8 did not all run") and all 6 controls for P6 to P8: 5 report "driver passed", and 1 fails with another message.
    • A driver that echoes the token or the session fails 12 or 13 scenarios with "leaked a secret".
    • A driver that does not kill its port-forward fails 13 scenarios.
    • A driver without the claim-delete and pool-delete checks of P8 fails the 2 controls for them.
  • The verifier's 13-case delete matrix (TestVerifierDeleteControls, run through a Go overlay at the new head) passes: only the hand delete of a released used member sends a delete build. Namespace deletion in both orders, control plane deletion, scale-down, pool deletion and orphan use send none.

  • Kind (cluster from hack/kind-dev.sh, kindest/node v1.32.0, Coder v2.37.2, a CI bootstrap replica with steps 7 to 25, operator built from this branch): the driver passed P1 to P8 on the first try.

    Phase Time
    P1 11 s
    P2 14 s
    P3 13 s
    P4 12 s
    P5 15 s
    P6 (Used=True 62 s after the usage request) 73 s
    P7 12 s
    P8 39 s
    cleanup 1 s

    P6 to P8 add about 2 minutes against PR 12 (R6), far below the 10-minute limit of decision D8. The CI job time is in a comment below.

  • Dogfood: a terminal recording and Coder UI screenshots follow in a comment.

Risks

  • R1 checked: it fails for the workspace page, tracked in CoderWorkspacePool: a dashboard page view marks an unclaimed member Used #444. Measured on Kind with Coder v2.37.2:

    • An admin who opens an unclaimed workspace's page in the Coder dashboard moves its last_used_at. The member turns Used=True at the next usage flush, and the pool releases it and builds a replacement. I reproduced it twice with agent-browser.
    • One GET /api/v2/workspaceagents/{id}/containers without a browser also moved it. The page sends that request.
    • A control member that nobody visited kept last_used_at for 2.5 minutes, and the workspaces list page did not move it.

    The result is no data loss. A released workspace costs one running workspace until an admin deletes its member, and with this PR that delete removes it in Coder. The how-to now warns about the dashboard page. The 30-minute idle half of the R1 check is in a comment below.

  • A person who deletes a used member before the pool released it still keeps its workspace. The window is short, because the pool releases it at its next reconcile.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

…ling)

Change-Id: Icbcdea82803da826274cfa54cdf5bc75bb3bee1f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…n deletes

Change-Id: Id610170c60d395917e4d9aa2d948aaa901a9773f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…eletes, dry run)

Change-Id: I4f77797d3e5332837b0f0b46aa75a2d208816b54
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…and negative checks

Change-Id: Ifdd690b257cab683e9305d2d7e719fd900c50719
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Iff29b554de2fcfbea163ac133d1c34e4137a5416
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: Icbc53b9f2f0f9048984ea5fdac4c74e6c3e30bf5
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I857c89ce10a28568122f86ff89e460fe4eb7d430
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Change-Id: I60abf0f28bb0fbb7c332d2d9f87faf6394ef7bf3
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T20:43:27.576175Z 44d0881 Manual request
🔒 Security Review ✅ Completed 2026-10-31T20:42:37.891308Z 44d0881 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33

ThomasK33 commented Oct 31, 2026 •

Copy link
Copy Markdown
Member Author

Dogfood on a hack/kind-dev.sh cluster (kindest/node v1.32.0, Coder v2.37.2, operator built from this branch at bcc256d, the same Go code as the head). I ran P6 to P8 by hand with a separate pool dog (3 replicas, one Reserve claim) and recorded the terminal. Screenshots show the Coder workspaces list of the pool owner, taken with agent-browser as an admin.

P6 to P8 by hand. The usage request made the member Used=True and released it after 32 s, and the pool built back to 3 ready unclaimed members. A scale to 1 deleted the 2 extra unclaimed workspaces in Coder (include_deleted read: delete/succeeded) and kept the unclaimed, the bound and the used workspace. The claim delete deleted its workspace. The pool delete deleted the unclaimed workspace and kept the used one. Deleting the used member by hand deleted its workspace (decision B). The server dry run printed created (server dry run) and made no pool, member or workspace.

dogfood-p6-p8.mp4

After P6: 6 workspaces. The pool dog has 3 unclaimed, 1 bound and 1 used. The 6th belongs to the R1 idle member of pool r1.

P6: the used workspace stays next to 3 rebuilt unclaimed workspaces

After the scale to 1: the kept unclaimed, the bound, the used and the R1 workspace.

P7: scale to 1

After the claim and pool deletes: only the used workspace and the R1 workspace stay.

P8: after the pool delete the used workspace stays

After the used member delete: only the R1 workspace stays.

P8: deleting the used member deleted its workspace

R1 (#444). Opening an unclaimed workspace's page as an admin moved its last_used_at, and the member turned Used. This is the page I opened:

R1: the workspace page of an unclaimed member

The 3 members that the R1 reproductions released stayed in Coder until I deleted the members by hand. Each delete then deleted the workspace (decision B):

dogfood-r1-leftovers.mp4

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@ThomasK33

Copy link
Copy Markdown
Member Author

CI evidence on 4a8894b1 (run 41228265505, job 124617646800). The full Kind E2E ran, and hack/e2e-workspace-pool.sh executed P1 to P8. It was not a skip notice.

Phase Time
P1 owner, template TTL, OwnerNotAllowed, allowlist 5 s
P2 2 ready members, /scale, deadlines 6 s
P3 scale to 3, 101 refused 6 s
P4 claim Bound and Ready, one bind extension 7 s
P5 operator restart while a member creates 15 s
P6 usage, then Used=True and released 50 s after the request 55 s
P7 scale to 1, 3 workspaces deleted in Coder (include_deleted) 13 s
P8 claim delete, pool delete (used member stays), used member delete, server dry run 33 s
cleanup 0 s

The pool step took 2 min 20 s, and its offline stub test (14 scenarios) took 22 s. The E2E job took 13 min 45 s. The last merge-queue E2E job on main (run 41222720765, f8cb970f) took 12 min 27 s, with a 52 s pool step. So the job grew by about 1 min 20 s (R6), far below the 10-minute limit of decision D8. The lifecycle driver passed after the pool step.

R1, idle half: passed. A ready member of a separate pool with a connected, ready agent kept its last_used_at (its create time, 18:00:16) for 33 minutes without a page visit. Nothing marked it Used. I read it 8 times, from 18:02 to 18:33. The failing half (a dashboard page view) is in the PR body and #444.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 4a8894b102

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…y when a used member keeps its workspace

Change-Id: Ia203d6cdeae60c621614d5509839dde10a3a95bb
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

New head 5f6227d2 after an independent advisory pass on 4a8894b1. It adds an envtest case for a member without a controller whose use the delete finds first (it keeps its workspace), two stub controls for the P8 claim-delete and pool-delete checks, and a how-to sentence about a used member that the pool has not released yet. No change to controller logic.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f6227d206

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/how-to/workspace-pools.md Resolved
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 5f6227d206

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…pace

Change-Id: I107ca9ba227caa96bf5b5182eb9bfeab5361d693
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

New head 16bdb94c: docs and comments only. It fixes the P2 finding on 5f6227d2 (namespace deletion usually keeps the workspace, because the control plane is deleted with it).

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 16bdb94c5e

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 16bdb94c5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33

Copy link
Copy Markdown
Member Author

Record for head 16bdb94c.

  • Required CI is green. The full Kind E2E (run 41236442383, job 124641780684) ran P1 to P8: P6 57 s, P7 6 s, P8 31 s. The pool step took 2 min 12 s and the job 13 min 18 s, against 12 min 27 s on main.
  • Codex code review: "Didn't find any major issues". Codex security review: "No security issues were found". Both name 16bdb94c, and 0 review threads are open.
  • Local gates passed on 16bdb94c: verify-vendor, test, build, lint, test-integration, mkdocs strict, markdownlint, cspell. make test-scripts (16 pool scenarios) passed on 5f6227d2, and the hack files have not changed since.
  • Reviews used: 6. Codex normal and security on 3 heads (4a8894b1 both clean, 5f6227d2 1 docs P2 that I fixed, 16bdb94c both clean). One independent advisory pass on 4a8894b1 recommended "ready with tracked follow-ups". I fixed its one should-fix in 5f6227d2.
  • Deferred: CoderWorkspacePool: a dashboard page view marks an unclaimed member Used #444. A dashboard page view, through the agent containers request, marks an unclaimed member Used.
  • I stopped because both reviews and CI are clean on this exact head.

…ber (failing)

Change-Id: I4a42997354c01978a509a53b96a09dc56b2973a8
Signed-off-by: Thomas Kosiewski <tk@coder.com>
…ing deleted

Change-Id: I3749d3adb9be216160caa0bd845f90180510fa21
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

New head 44d08812. An independent verifier blocked 16bdb94c. Namespace deletion has no fixed order, so the namespace controller could delete a released Used=True member before it marked the CoderControlPlane. The cleanup then started a Coder delete build. Now a released used member reads its namespace without the cache before any Coder call. If the namespace is being deleted, it releases with no Coder call. A failed read keeps the finalizer. Tests cover both deletion orders and the failed read, and the hand-delete case still deletes. The docs say that namespace deletion keeps a used member's workspace.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 44d0881227

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 44d0881227

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33

Copy link
Copy Markdown
Member Author

Record for head 44d08812, after the independent verifier blocked 16bdb94c.

  • Blocker: a namespace deletion could reach a released Used=True member before the CoderControlPlane, and the cleanup then sent a Coder delete build. I fixed it in 44d08812. A released used member now reads its namespace without the cache before any Coder call. If the namespace is being deleted, the member releases with no Coder call. A failed read keeps the finalizer and returns the error. The test failed before the fix (d6c700a0: member first and read failure red). The verifier's 13-case delete matrix passes at the new head through a Go overlay: only the hand delete of a released used member sends a delete build.
  • Required CI is green. The full Kind E2E (run 41239971296, job 124652333565) ran P1 to P8: P6 49 s, P7 6 s, P8 29 s. The pool step took 2 min 1 s and the job 14 min 12 s. The lifecycle driver passed after the pool step.
  • Codex code review: "Didn't find any major issues". Codex security review: "No security issues were found". Both name 44d08812, and 0 review threads are open.
  • Local gates passed on 44d08812: verify-vendor, test, build, lint, test-integration, mkdocs strict, markdownlint, cspell. The hack files have not changed since 5f6227d2, where make test-scripts passed.
  • Fix rounds: 3 of the cap of 4. Deferred: CoderWorkspacePool: a dashboard page view marks an unclaimed member Used #444.

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 692da1d Oct 31, 2026
12 of 13 checks passed
@ThomasK33
ThomasK33 deleted the pool-e2e-p6-p8 branch October 31, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant