fix(spurctld): keep the node record when its agent shuts down - #771
Closed
pre wants to merge 1 commit into
Closed
Conversation
spurd deregisters on SIGTERM, and the controller answered with remove_node(force = true), which deleted the node record. On a cluster with the WireGuard mesh, mesh_from_nodes builds the membership from that record, thus the k0s reconciliation pruned the peer of the node on every other node. The rebooted node could not register again, because the controllers are only reachable over the mesh it had just left. This is a deadlock: no part of it repairs itself, and the node stays out of Kubernetes and out of the batch cluster. A stopping agent is Down, not gone. DeregisterAgent now marks the node Down and keeps the record, its wg_pubkey and its mesh IP. The jobs are evicted exactly as before, because the Down transition evicts them. An admin hold keeps its reason and its lock. check_node_health recovers the node when its heartbeat returns. Removal stays an operator action: `spur node remove` and `spur k8s down`. Measured on a three-node cluster with k0s and the mesh: before the change, a reboot of a worker left sinfo at 2 nodes of 3, one peer on every node that stayed up, the Kubernetes node NotReady and spurd in a restart loop. After the change, two reboots in a row keep 3 nodes, 2 peers, the Kubernetes node Ready and no restart of spurd, and the node returns about 30 seconds after it comes up. The fault fires only when the rebooted node is not the Raft leader: the removal of the leader cannot commit while the leader goes down.
pre
requested review from
biluriuday,
sajmera-pensando,
sgopinath1,
shiv-tyagi and
yansun1996
as code owners
August 31, 2026 13:27
Author
|
Verification so far, since the workflow runs need maintainer approval: Local, on this branch alone (rebased on 360d44c):
On a three-node cluster with the WireGuard mesh and k0s, control plane on
The E2E suite has not run yet. Could a maintainer approve the workflow runs? |
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
On a cluster with the WireGuard mesh and k0s, rebooting a node takes it out of Kubernetes and out of the batch cluster, permanently. Nothing repairs itself, and no signal names the network:
sinfoNotReady, still after 5 minutesspurdon the rebooted nodeThe error on the rebooted node points at Raft, while the fault is in the mesh:
A reboot is a normal event, so a kernel update or a power cut loses a node for good.
Cause
spurdderegisters onSIGTERM(spurd/main.rs), andderegister_agentanswered withremove_node(force = true), which deletes the node record.mesh_from_nodesbuilds the WireGuard membership from that record, so the membership loses the node, andreconcile_meshthen removes its peer on every node that stayed up. The rebooted node cannot register again, because the controllers are only reachable at their mesh addresses — the mesh it was just removed from. The local controller answers, is not the leader, and cannot forward to the leader over the broken mesh, which produces the message above.The comment in
cluster_k8s.rssays the prune exists so that node-local drift, "reboot, wg restart", can self-heal. A reboot is the case the prune makes unrecoverable.The fault fires only when the rebooted node is not the Raft leader: the removal of the leader cannot commit while the leader goes down, so rebooting the leader is survivable by accident. On a cluster of N nodes, N-1 of them break the cluster when they reboot.
Change
A stopping agent is Down, not gone.
DeregisterAgentnow marks the node Down and keeps the record, itswg_pubkeyand its mesh IP:Downtransition evicts them.check_node_healthrecovers the node when its heartbeat returns.spur node removeandspur k8s down.Test
agent_shutdown_marks_node_down_and_keeps_itfails if the record, thewg_pubkeyor the automatic recovery is lost. 963 unit tests pass.Verified on a three-node cluster with k0s and the WireGuard mesh, the k0s control plane on node 2 and a worker on node 3:
node removed from clusteron the survivorsmarked DOWN, then recovered about 30 s latersinfoNotReadyReady, it never leftspurdrestartswg syncconfneededTwo reboots in a row, plus a batch job afterwards. A reboot of all three nodes at the same time also comes back complete.
Not in this PR
The agent still has no path to a controller that does not go through the mesh:
spur.confnames only mesh addresses. This change removes the fault; an underlay fallback for registration would remove the whole class, and is a larger change worth its own discussion.