Skip to content

Prod deploy evicted from the merge queue when an unrelated Node write breaks the cordon-claim CAS test #2976

Description

@devantler

🤖 Generated by the Agentic Engineer

Evidence

The prod deploy fails intermittently in 🔑 Stage Flux and consumer GHCR pull credential, evicting the PR from the merge queue:

##[error]Could not atomically claim and cordon Talos node autoscale-cx33-12ac9ae5b0b9effe; refusing to drain it.
cordon-claim: The request is invalid: the server rejected our request due to an error in our request

Measured on 2026-08-05 across ci.yaml runs: 4 merge_group failures interleaved with 8 successes on unchanged code —

run PR time
30997196586 pr-2970 10:23Z
31003661088 pr-2971 11:58Z
31006527422 pr-2970 12:38Z
31011548385 pr-2971 13:43Z

Intermittency on unchanged code rules out a malformed patch, which would fail every time.

Diagnosis

claim_node_cordon_ownership (scripts/refresh-flux-ghcr-auth.sh) builds an RFC 6902 patch whose first op is

{"op": "test", "path": "/metadata/resourceVersion", "value": "<version read earlier>"}

A failed JSON Patch test op is exactly what the API server rejects as 422, which kubectl renders as "The request is invalid: the server rejected our request due to an error in our request".

That test asserts nothing whatsoever changed on the Node object, but a Node has several independent writers. Read live from the failing node's managedFields:

manager operation last write
kubelet Update status 15:21:32Z (≈3 min before sampling)
kube-controller-manager Update 14:01:21Z
Go-http-client Update 14:01:40Z
hcloud-cloud-controller-manager Update status 03:17:32Z
cilium-operator-generic Update status 03:17:32Z

So a routine kubelet status heartbeat — which has no bearing on whether the node is safe to cordon and drain — invalidates the claim and fails the whole deploy. There is no retry: claim_node_cordon_ownership is called once per node and returns non-zero on the first conflict.

The guard is simultaneously too strict (any unrelated status write aborts it) and only indirectly protective: resourceVersion is a proxy for "the invariants I validated still hold", not the invariants themselves.

Why it matters

This is the delivery path. Every eviction costs a full merge-queue cycle, and because the trigger is an unrelated background write, re-queuing without a fix is a coin flip — the exact "re-queue without root-causing" trap the contract warns about. It is not specific to the two dependency PRs that surfaced it; any PR can hit it.

Expected behaviour

The claim survives writes that do not affect drain safety, and still refuses when a genuinely conflicting change occurred.

Acceptance criteria

  • A CAS conflict on the claim patch is retried (bounded, with the node re-read and the caller's captured invariants re-validated on each attempt) rather than failing the deploy on first conflict.
  • The claim no longer aborts on writes irrelevant to drain safety. Either narrow the test ops to the actual invariants — metadata.uid, spec.unschedulable == captured was_cordoned, spec.taints == captured initial_taints, and the owner annotation being unclaimed/ours — or keep resourceVersion but re-read and re-validate on retry.
  • Exhausting the retry budget still fails closed with the existing refusal message; the safety property is unchanged.
  • RED/GREEN proof in scripts/tests/refresh-flux-ghcr-auth/ using the existing fake kubectl: a first-attempt 422 followed by success must fail before the fix and pass after, and a persistent conflict must still refuse to drain.

Size

Small–medium. Contained to claim_node_cordon_ownership and its two call sites, plus tests.

⚠️ This touches the path that drains prod nodes. The retry must re-validate rather than blindly re-patch — a retry that skips revalidation would convert a safe refusal into an unsafe drain, which is strictly worse than the current flake.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

Status
✅ Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions