0042. Reset is opt-in per removal, named partitions only, and idempotent by recording after the ack
Status: Accepted · Date: 2026-10-26 · Design section: Nodes (removal), Roadmap phase 3
Decided: Add an opt-in ichor.maesh.dev/reset-on-delete annotation that, after a removal's etcd member is gone, wipes only STATE and EPHEMERAL by label and reboots the node to maintenance mode -- holding the finalizer if Reset fails, and recording the request only after Talos has acknowledged it, so a resumed reconcile never asks twice
Context
ADR 0032 completed TalosMachine removal as far as etcd: drain, leave
etcd, clear the finalizer. What it left unresolved, and said so out loud
with a Warning Event (NodeStillHasClusterCredentials): the physical
node was never reset, so it still holds the secrets bundle's PKI even
after Ichor has forgotten about it entirely. That gap is this ADR's
reason to exist.
Architecture.md's "Nodes" section already investigated Talos's Reset
call and recorded two facts, not guesses:
The reset must name the partitions to wipe. With its default mode and no partitions named, Reset wipes the entire system disk, boot and EFI included, which means a reinstall rather than a return to maintenance mode. Passing STATE and EPHEMERAL explicitly is what leaves the installed system in place and brings the node back up with no configuration.
Reset is fire-and-forget. It returns an actor id immediately and the sequence runs asynchronously, so status has to record that a reset was requested before the controller can crash -- otherwise a resumed reconcile fires a second concurrent reset at the same node.
Both are design-level constraints this unit has to satisfy, not implement around.
Decision
Reset is irreversible, so it is opt-in per removal, not automatic
A new annotation, ichor.maesh.dev/reset-on-delete (value "true"),
gates whether reconcileRemoval ever calls Reset at all -- the same
shape deleteguard.AllowDeleteAnnotation already uses: a deliberate,
per-object decision rather than a default, because an operator may
legitimately want the disk left alone to investigate a failure, or intact
to reuse for something else. Its absence changes nothing from ADR 0032:
etcd member removed, finalizer cleared, NodeStillHasClusterCredentials
fired exactly as before.
TalosClient grows one more method, verified against machinery v1.14.3's own source
Reset(ctx context.Context) error adapts *client.Client.ResetGeneric
with a fixed, never-caller-configurable request:
&machineapi.ResetRequest{
Graceful: false,
Reboot: true,
SystemPartitionsToWipe: []*machineapi.ResetPartitionSpec{
{Label: constants.StatePartitionLabel, Wipe: true},
{Label: constants.EphemeralPartitionLabel, Wipe: true},
},
}
SystemPartitionsToWipeis never empty and never caller-supplied:ResetPartitionSpec's own doc comment says plainly that an empty list means "all the partitions are erased," and the machined server (internal/app/machined/internal/server/v1alpha1/v1alpha1_server.go) rejectsMode: USER_DISKStogether with a non-emptySystemPartitionsToWipe-- confirmingSTATE+EPHEMERALby label,Modeat itsALLzero value, is exactlytalosctl reset --system-labels-to-wipe STATE,EPHEMERAL's own usage, not a novel combination.constants.StatePartitionLabel/EphemeralPartitionLabelare used rather than the literal strings, so a future machinery bump renaming either constant breaks this at compile time instead of silently wiping the wrong (or no) partition.Graceful: false: by the time this is ever called,removeEtcdMemberhas already drained the node and left etcd itself (ADR 0032). Talos's own graceful pre-reset routine exists to do exactly that when nothing else has, which is never true here -- architecture.md's own "belt rather than trousers" framing for Ichor's drain/leave applies the other way round to this flag: Ichor's own sequence is the trousers here, Talos's graceful-reset is the belt this call has no use for.Reboot: true: required for the "machine reappears as pending and can be reused" story architecture.md's removal section describes -- a halted node needs a human to power it back on before discovery can see it again, which defeats the point of resetting it automatically.
No second interface: this is the same TalosClient ADR 0032 widened,
reusing the same r.TalosClient factory function a third time (self,
survivor, and now self again for Reset).
Order: etcd leave, then Reset, then the finalizer -- Reset failure holds it
resetNode runs after removeEtcdMember succeeds and before
completeRemoval. A failed Reset records reasonResetFailed on the
Removal condition and returns without clearing the finalizer -- the
same "stay, don't disappear" shape every other refusal in this file
already has, and the one that matters most here: a TalosMachine that
opted into Reset but whose Reset request never actually reached the node
must not be allowed to vanish from Ichor while the physical hardware is
untouched and still credentialed.
Idempotency: recorded after the RPC's own ack, not before
Reset is fire-and-forget -- the RPC returns once Talos has accepted the
request and started the wipe-and-reboot sequence in a background
goroutine (confirmed by reading v1alpha1_server.go's Reset handler:
go func() { s.Controller.Run(resetCtx, runtime.SequenceReset, &opts) }()
runs after the response is already built), not once the sequence
finishes. resetNode therefore checks conditionRemoval's own status
before calling Reset, and writes reasonResetRequested on it
immediately after Reset's RPC returns without error -- the earliest
point at which "a reset was requested" is actually true, and
architecture.md's own fix for "a resumed reconcile fires a second
concurrent reset": a crash after this write means the next reconcile
sees the condition already True and returns without calling Reset
again at all.
Two notes against calling Reset twice, read from Talos's own source
(not inferred), for whoever next has to reason about how dangerous a
near-simultaneous double-call actually would have been if this check
were ever bypassed: Controller.Run takes a priorityLock before
running any sequence, so a second SequenceReset arriving while the
first is still in flight blocks (up to a minute) rather than racing the
first on the same disk; and once a first Reset actually completes with
Reboot: true, the node reboots into fresh maintenance mode, where the
admin certificate any second attempt would authenticate with is no
longer trusted at all -- the second call fails closed rather than
repeating the wipe. Neither of these is why this ADR's own check exists
(the architecture doc's instruction is unconditional, and this project
does not rely on a dependency's internal locking as its own safety net),
but both explain why no real-hardware attempt at forcing a genuine double
Reset was expected to find anything more alarming than a clean refusal.
The Warning Event is wrong once Reset actually succeeded, so it is skipped
completeRemoval takes a reset bool. When true, the success message
says the node was reset and is rebooting to maintenance, and
NodeStillHasClusterCredentials is not emitted -- it would be false:
Reset just wiped the STATE partition those credentials lived on. The
Event fires exactly when it did before this ADR, in exactly the case
this ADR did not change.
Consequences
- Removal has a real way to close the gap ADR 0032's own Warning Event named, but only when an operator explicitly asks for it per machine -- the default for every existing and future removal stays unchanged.
resetNodereusesconditionRemovalrather than introducing a fourth condition type:IsStatusConditionTrueon it, checked before any destructive call in this file, now means "something terminal-shaped already happened here, do not redo it" uniformly across removeEtcdMember's sibling check, resetNode's own check, and completeRemoval's final write.- A removal that opts into Reset and succeeds takes one more Talos RPC
than one that does not, on the same node it was already dialing for
removeEtcdMember's self-leave attempt. - Nothing here touches
forceRemove(reset on a node that cannot be dialed at all): opting into Reset when the node is unreachable simply fails at the dial, same as any other Talos call in this file, and holds the finalizer exactly like any other failed step.
Real-hardware verification
Run on 2026-10-26 against the 3-node HA cluster (Talos v1.14.3,
ichor-e2e namespace, the real controller binary running as a pod, ADR
0007). One node, ichor-e2e-ha-1, was used for both cases in turn: it had
already left etcd in an earlier run (etcd listed only ha-0), so deleting
its TalosMachine exercised the drain, the etcd-already-gone path and,
when opted in, Reset, without touching quorum. A TalosMachine is created
approved with allow-delete, waited on until the removal finalizer is
present, then deleted. State was read over the Talos API from a pod, with
the cluster's own admin credentials.
- Without the annotation. Before:
ha-1machinestatusstagerunning,STATEandEPHEMERALvolumesready. After the delete completed: a newNodeStillHasClusterCredentialsWarning Event on the new object (new UID), and the node was unchanged -- stagerunning,STATEandEPHEMERALstillready, the admin credentials still authenticating. This is what "opt-in" means: nothing is wiped and the Event keeps firing. - With
ichor.maesh.dev/reset-on-delete: "true". The delete completed and noNodeStillHasClusterCredentialsEvent was emitted for the new object (the Event list held exactly the two earlier ones). The node came back in stagemaintenancewithSTATEmissing(METAstillready), and the admin credentials were refused withx509: certificate signed by unknown authority: it no longer holds the cluster's PKI, and the disk was not reinstalled (the node rebooted to maintenance rather than needing an install), as the named-partitions design intends.EPHEMERALis not listed in maintenance; it wasreadybefore.
Not observed: the Removal condition's ResetRequested reason (the object
is gone once the finalizer clears and nobody was watching mid-flight), the
ResetFailed hold (no failing node was available), and the
second-call-is-a-no-op path against a live node. Those stay covered by
internal/controller/reset_test.go only.
Amendment 2026-11-05: an optional full wipe
The default stays as decided here: STATE and EPHEMERAL by label, nothing else. A node that has to be installed afresh (a SecureBoot move, ADR 0123) cannot use it, because the installed boot partition survives and the node boots its installed system again. TalosOperation.spec.node.wipeMode (system-disk or all, only with the reset action; ichor node reset --wipe-mode) therefore asks for the other shape of ResetRequest: Mode SYSTEM_DISK or ALL and no systemPartitionsToWipe, which Talos reads as the whole system disk (and, for ALL, every user disk). The mode travels to the removal flow in the machine annotation ichor.maesh.dev/reset-wipe-mode, next to reset-on-delete; reclaim.ResetRequestWipe is the one place that builds the request, and MachineReclaim is unchanged (it always wipes STATE and EPHEMERAL). Needs the same override and typed name as any reset. Talos reference: talosctl reset --wipe-mode all|system-disk|user-disks (docs.siderolabs.com/talos/v1.14).