Repository navigation
fix(apps/kubernetes): pre-delete hook hangs deleting tenant addon HelmReleases when the tenant has no working nodes #3271
Description
Summary
The kubernetes app's pre-delete hook (packages/apps/kubernetes/templates/delete.yaml, Job <release>-pre-cleanup) can hang indefinitely deleting a tenant's in-cluster addon HelmReleases when the tenant cluster has no working nodes — e.g. LINSTOR/ZFS resource exhaustion, worker VMs that never booted/joined, or a nodeGroup at minReplicas: 0. Because the hook's HelmRelease deletion is an unbounded kubectl delete … --wait=true, the tenant Kubernetes CR never finalizes, so deleting a broken tenant stalls (and, under GitOps, wedges the helm-controller working on the stuck uninstalls).
Root cause
delete.yaml Step 2:
kubectl -n <ns> delete helmreleases.helm.toolkit.fluxcd.io \
-l "cozystack.io/target-cluster-name=<release>" \
--ignore-not-found=true --wait=true # <-- no --timeoutThe child HelmReleases (in-tenant cilium, coredns, csi, ingress-nginx, …) can only uninstall cleanly if the tenant cluster is reachable and schedulable. When the tenant has no Ready nodes, the in-tenant cilium HelmRelease sits in InstallFailed (cilium-operator Deployment never leaves InProgress — nothing to schedule it on), and its uninstall/remediation drags for many minutes or does not converge. --wait=true with no timeout blocks the pre-delete Job on that, so:
<release>-pre-cleanupJob never reaches "completed"- Helm uninstall of the parent
kubernetes-<release>HelmRelease fails:timeout waiting for: [Job/<ns>/<release>-pre-cleanup status: 'InProgress'] - the
KubernetesCR finalizer never runs to completion → the CR (and its namespace) stays stuck deleting.
Contrast: Step 4 (TenantControlPlane delete) already does the right thing — --wait=true --timeout=120s || echo WARNING … — so it degrades gracefully. Step 2 (and, to a lesser extent, Steps 3/5/6) do not.
Impact
- Operators: deleting a tenant Kubernetes cluster that never became healthy (storage exhausted, nodes never joined, scaled to 0) can hang teardown and leave the namespace
Terminating. - CI: the render-side
kubernetes-oidc-system/kubernetes-oidc-customconfige2e suites (which create a control-plane-only tenant with 0 workers and delete it immediately) deterministically hit this. Under the Chainsaw suite (test(e2e): migrate app suite from BATS to Chainsaw and wire into CI #2826), which enforces cleanup completion, both fail atcontext deadline exceededafter the 5 min cleanup budget. The legacy BATS suite masked it (kubectl delete … --timeout=2m 2>/dev/null || true), so it was invisible onmain.
Evidence
- PR test(e2e): migrate app suite from BATS to Chainsaw and wire into CI #2826, E2E run 29021040367 (attempt 2). cozyreport
summary.txt:
UninstallFailed helmrelease/kubernetes-oidc-byo — Helm uninstall failed … timeout waiting for: [Job/tenant-test/kubernetes-oidc-byo-pre-cleanup status: 'InProgress'] - Child HR stuck:
kubernetes-oidc-byo-cilium — InstallFailed … timeout waiting for: [Deployment/cozy-cilium/cilium-operator status: 'InProgress'](no nodes → operator never schedules). - The TenantControlPlane itself did eventually delete (~10 min), confirming it's teardown-slowness on the child HRs, not a Kamaji DataStore deadlock.
Proposed fix (directions, for discussion)
Make the pre-delete hook resilient to an unreachable/nodeless tenant so teardown always completes:
- Bound Step 2's
kubectl delete helmreleases … --wait=truewith a--timeoutand|| echo WARNING …(mirror Step 4). - After the bounded wait, for any child HRs still present (uninstall couldn't converge because the tenant is gone/nodeless), strip the Flux
finalizersso they can be GC'd — safe here because the whole tenant (and its control plane) is being destroyed, so there is nothing left to orphan. - Apply the same bounded-wait treatment to Steps 3/5/6 for consistency.
This is a delicate hook with teardown-ordering history (#3062, #3078) — the finalizer-force in (2) needs care to not fire while the tenant is still legitimately alive (gate it on the TCP being confirmed gone, as Step 4b already does for the datastore secret).
Acceptance
- Deleting a
KubernetesCR whose tenant has no Ready nodes completes teardown within a bounded time (no indefinite hang, noTerminatingnamespace). - The two render-side OIDC e2e suites tear down within the Chainsaw cleanup budget.
Activity
fix(apps/kubernetes): bound the pre-delete hook's tenant HelmRelease …
test(e2e): migrate app suite from BATS to Chainsaw and wire into CI (#…
myasnikovdaniil commentedon Jul 14, 2026
Resolved by
02b87b63f("bound the pre-delete hook's tenant HelmRelease wait"), merged tomainvia #2826. Closing — it stayed open only because that PR carried noCloses.The demonstrated hang was Step 2's unbounded
kubectl delete helmreleases … --wait=true: the tenant's child addon HelmReleases carry a Flux finalizer that only clears once helm-controller can uninstall inside the tenant, which is impossible with no Ready nodes.02b87b63fbounds that wait with--timeout=120sand force-clears the leftover Flux finalizer on any child HR still present, so teardown of a nodeless tenant now completes within a bounded time (and the render-sidekubernetes-oidc-{system,customconfig}Chainsaw suites tear down within budget).Steps 3/5/6 were investigated as a possible follow-up (#3274) but target management-cluster-finalized resources (Kamaji/CAPI, CDI + the CSI driver, the LB controller) that don't depend on tenant node health, so they don't exhibit this hang — Step 3 in particular is a no-op, since the KamajiControlPlane carries no finalizer in this chart. #3274 is closed as unnecessary.
fix(e2e): give the kafka teardown delete its own timeout
fix(e2e): give the kafka teardown delete its own timeout
fix(e2e): give the kafka teardown delete its own timeout