Skip to content

Commit ac9f501

Browse files
fix(uninstall): make uninstall idempotent on a half-uninstalled node
Two load-bearing steps failed on every retry once their targets were already gone: kubeadm reset exited 127 (kubeadm binary absent), and purge packages exited 100 ('Unable to locate package') once the k8s apt repo had been removed by a prior partial run. Guard kubeadm reset with 'command -v kubeadm'; purge only the packages dpkg still tracks (dpkg-query). The rm -rf wipes now stop kubelet/containerd first, lazy-unmount live pod-volume mounts under /var/lib/kubelet, and assert the target is actually gone, so a busy mount / immutable file can't wedge a permanent partial uninstall. Verified live on a previously-stuck box: uninstall now completes + reboots.
1 parent bab28d6 commit ac9f501

2 files changed

Lines changed: 48 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77
The release pipeline extracts the section matching the pushed tag (`## vX.Y.Z`)
88
as the GitHub release notes, so every released version needs a section here.
99

10+
## v1.5.3
11+
12+
### Fixed
13+
- **`runos uninstall` is now idempotent — it no longer wedges on a half-uninstalled
14+
node.** Two load-bearing steps failed on *every* retry once their targets were
15+
already gone: `kubeadm reset` exited 127 when kubeadm was absent, and `purge
16+
packages` exited 100 ("Unable to locate package") once the Kubernetes apt repo had
17+
been removed by a prior partial run, so apt could no longer resolve the names. Both
18+
now treat "already removed" as success: kubeadm reset is guarded by `command -v
19+
kubeadm`, and the purge targets only the packages dpkg still tracks. The `rm -rf`
20+
wipes now stop kubelet/containerd first, lazy-unmount any live pod-volume mounts
21+
under `/var/lib/kubelet`, and assert the target is actually gone, so a busy mount or
22+
immutable file can no longer turn a re-runnable cleanup into a permanent "partial
23+
uninstall". Net effect: `runos uninstall` succeeds (and clears `/etc/runos`, then
24+
reboots) regardless of how partially-uninstalled the node already was.
25+
1026
## v1.5.2
1127

1228
### Fixed

commons/uninstall.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,28 @@ func Uninstall(full bool) error {
6363

6464
// --- Kubernetes (load-bearing) -----------------------------------------
6565
// kubeadm reset can hang on a wedged container runtime / etcd, so bound it.
66+
// Only reset if kubeadm is actually installed: an absent kubeadm means there
67+
// is nothing to reset (the host is already clean), NOT a failure. Without this
68+
// guard a re-run on a half-uninstalled box wedges forever — `timeout` can't
69+
// exec the missing kubeadm (exit 127), which marked this load-bearing step
70+
// failed and made `runos uninstall` report a partial uninstall on every retry.
6671
roslog.Print("Removing Kubernetes... ")
67-
critical("kubeadm reset", "timeout 120 kubeadm reset -f")
68-
// Remove cluster + etcd data (load-bearing: leftover etcd data is the worst
69-
// thing to silently keep on a "uninstalled" node).
70-
critical("wipe /etc/kubernetes", "rm -rf /etc/kubernetes")
71-
critical("wipe /var/lib/kubelet", "rm -rf /var/lib/kubelet")
72-
critical("wipe /var/lib/etcd", "rm -rf /var/lib/etcd")
72+
critical("kubeadm reset", "if command -v kubeadm >/dev/null 2>&1; then timeout 120 kubeadm reset -f; fi")
73+
// Stop kubelet + the container runtime before wiping their data dirs so nothing
74+
// holds them open. kubeadm reset does this when present, but it may be absent on a
75+
// half-uninstalled box (the guard above skips it), so do it explicitly. Best-effort.
76+
step("timeout 30 systemctl stop kubelet || true")
77+
step("timeout 30 systemctl stop containerd || true")
78+
// Remove cluster + etcd data (load-bearing: leftover etcd data is the worst thing
79+
// to silently keep on an "uninstalled" node). Each wipe ASSERTS the target is
80+
// actually gone (`[ ! -e ... ]`): a bare `rm -rf` exits non-zero on a busy mount or
81+
// immutable file, and as a load-bearing step that would wedge the uninstall as a
82+
// permanent "partial uninstall" on every retry. /var/lib/kubelet can hold live
83+
// pod-volume mounts (SA-token / secret / emptyDir tmpfs), so lazy-unmount
84+
// everything under it (deepest first) before removing, or `rm` fails "device busy".
85+
critical("wipe /etc/kubernetes", "rm -rf /etc/kubernetes; [ ! -e /etc/kubernetes ]")
86+
critical("wipe /var/lib/kubelet", "awk '$2 ~ \"^/var/lib/kubelet\" {print $2}' /proc/mounts | sort -r | while read -r m; do umount -lf \"$m\" 2>/dev/null || true; done; rm -rf /var/lib/kubelet; [ ! -e /var/lib/kubelet ]")
87+
critical("wipe /var/lib/etcd", "rm -rf /var/lib/etcd; [ ! -e /var/lib/etcd ]")
7388
step("rm -rf ~/.kube || true")
7489
// CNI configurations (best-effort)
7590
step("rm -rf /etc/cni || true")
@@ -127,10 +142,17 @@ func Uninstall(full bool) error {
127142
roslog.Print("Removing packages... ")
128143
// Unhold the held Kubernetes packages so they can be purged (best-effort).
129144
step("apt-mark unhold kubelet kubeadm kubectl || true")
130-
// Remove ALL RunOS-installed packages in a SINGLE non-interactive apt-get
131-
// (was five separate, slow, lock-contending invocations — the long delay).
132-
// Bounded by the dpkg-lock timeout above plus an overall `timeout`.
133-
critical("purge packages", "DEBIAN_FRONTEND=noninteractive timeout 300 "+aptGet+" remove --purge dnsmasq kubeadm kubectl kubelet kubernetes-cni containerd wireguard wireguard-tools haproxy")
145+
// Purge ALL RunOS-installed packages in a SINGLE non-interactive apt-get (was
146+
// five separate, slow, lock-contending invocations — the long delay). Bounded
147+
// by the dpkg-lock timeout above plus an overall `timeout`.
148+
//
149+
// Purge only the subset dpkg still tracks (installed or residual-config). Once
150+
// the k8s apt repo is removed — a best-effort step just below, which a PRIOR
151+
// partial uninstall may already have run — `apt-get remove kubeadm ...` fails
152+
// with "Unable to locate package" (exit 100) for the now-unknown names, which
153+
// wedged the uninstall on every retry. dpkg-query lists the present names; if
154+
// none remain there is nothing to purge and the step is a clean no-op.
155+
critical("purge packages", "pkgs=$(dpkg-query -W -f='${Package}\\n' dnsmasq kubeadm kubectl kubelet kubernetes-cni containerd wireguard wireguard-tools haproxy 2>/dev/null); if [ -n \"$pkgs\" ]; then DEBIAN_FRONTEND=noninteractive timeout 300 "+aptGet+" remove --purge $pkgs; else echo 'no RunOS packages present to purge'; fi")
134156
step("DEBIAN_FRONTEND=noninteractive timeout 120 " + aptGet + " autoremove || true")
135157
step("apt-get clean || true")
136158
// Remove Kubernetes apt repo (best-effort)

0 commit comments

Comments
 (0)