Skip to content

Commit 03fb11c

Browse files
committed
fix(talosctl): reboot after a failed drain during upgrade
talosctl upgrade writes the new installer image to disk before it drains the Kubernetes node and reboots into it. When the drain step failed - for example because the apiserver is unreachable or the cluster is not yet bootstrapped - the command returned early, before the reboot. That left the upgrade half-applied: the new image was staged on disk while the node kept running the old version, with no reboot to activate it and no signal to the operator, since the earlier progress output already read "upgrade completed". The next unrelated reboot would then silently boot into the staged image. Once the image is committed to disk, aborting before the reboot is the worst outcome. The drain is a best-effort courtesy and must not strand the upgrade, so warn about the failed drain and continue to the reboot so the staged image is actually applied. Draining cordons each node before evicting its pods, so a drain that fails after the cordon leaves the node unschedulable. drainNodes threw away the collected node map on error, so the deferred uncordon was skipped: the node rebooted into the new version but stayed SchedulingDisabled, and with the reboot succeeding the command still exited 0 - an orphaned cordon with no error surfaced. Return the partial map on error so the existing uncordon step restores the node to schedulable once it is Ready again. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
1 parent ea95578 commit 03fb11c

2 files changed

Lines changed: 19 additions & 2 deletions

File tree

cmd/talosctl/cmd/talos/drain.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ type nodeUpdate struct {
3030
// and performs cordon + drain on all of them in parallel.
3131
//
3232
// It returns a map of talosIP -> k8sNodeName for use in the uncordon phase.
33+
//
34+
// On error the map is still returned (partially populated): each node name is
35+
// recorded before that node is cordoned, so the map holds every node that may
36+
// have been cordoned before the failure. A caller that proceeds past a failed
37+
// drain (e.g. the upgrade path, which has already staged a new image on disk)
38+
// can then still uncordon those nodes instead of leaving them SchedulingDisabled.
3339
func drainNodes(ctx context.Context, clientFactory *global.ClientFactory, drainTimeout time.Duration, rep *reporter.Reporter) (map[string]string, error) {
3440
// For kubeconfig - build a random endpoint client (to go to the controlplane).
3541
c, err := clientFactory.BuildRandomEndpointClient(ctx)
@@ -103,7 +109,10 @@ func drainNodes(ctx context.Context, clientFactory *global.ClientFactory, drainT
103109
<-aggregatorDone
104110

105111
if err != nil {
106-
return nil, err
112+
// Return the partially-populated map alongside the error: it holds the
113+
// nodes that were cordoned before the failure, so a caller that continues
114+
// past a failed drain can still uncordon them.
115+
return k8sNames, err
107116
}
108117

109118
return k8sNames, nil

cmd/talosctl/cmd/talos/upgrade.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,15 @@ func upgradeViaLifecycleService(ctx context.Context, clientFactory *global.Clien
147147
if upgradeCmdFlags.drain {
148148
nodeNames, err = drainNodes(ctx, clientFactory, upgradeCmdFlags.drainTimeout, rep)
149149
if err != nil {
150-
return err
150+
// upgradeInternal above has already written the new installer image to disk;
151+
// the node only switches to it on the reboot below. Aborting here would leave
152+
// the upgrade half-applied: the new image is staged on disk, the node keeps
153+
// running the old version, and the next unrelated reboot silently boots into
154+
// the new one - all while the earlier progress output reads "upgrade completed".
155+
// A failed drain (e.g. the Kubernetes apiserver is unreachable, or the cluster
156+
// is not yet bootstrapped) is a best-effort courtesy and must not strand the
157+
// upgrade. Warn loudly and continue to the reboot so the staged image is applied.
158+
cli.Warning("failed to drain Kubernetes node(s) before reboot, continuing without drain: %s", err)
151159
}
152160
}
153161

0 commit comments

Comments
 (0)