You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bf5ad40
Browse filesBrowse the repository at this point in the historyBrowse files
test(e2e): guard the tenant addon HelmReleases against teardown
The remediation guard ran over the parent kubernetes-<test> HelmRelease
only, and the parent's .status.history says nothing about the addon
releases it installs. A teardown of one of them therefore surfaced as
whatever downstream deadline it eventually broke - a node that never
turns Ready, a PVC that never binds - with nothing in the run naming the
teardown that preceded it. The CNI and CSI releases are the sharp cases,
because their teardown removes a cluster-wide precondition rather than
churning a single component.
cozy_guard_helmrelease reads one release's history and judges it;
cozy_guard_addon_helmreleases runs it over the addon releases the parent
installs. Both the parent block and the addon loop go through the same
function, so the parent's reads are exercised by the same unit tests
rather than sitting inline in run_kubernetes_test, where a revert of
them stays green. The addon list comes from the cluster rather than from
a copy of the chart's addon set, so a release added later is covered
without touching the script, and a prefix that matches nothing is an
error rather than a quiet pass over nothing.
One rule decides the verdict: fail when the release was REMOVED although
it is configured never to remove itself to recover. An uninstalled
Snapshot is proof of removal, and two configurations write it: the
default strategy's install remediation, and an upgrade remediation set
to strategy: uninstall, which nothing under packages/ uses. So on a
RetryOnFailure release, which is the tenant cilium and csi and every
Application HelmRelease, that Snapshot has no explanation inside the
release and fails the run. On a release
still using the default strategy the same Snapshot is that strategy
doing its documented job, paired with retries: -1, and it is reported
instead. So is a failed Snapshot, which under RetryOnFailure is not even
remediation. Nothing in the suite measures how often those addons take
their configured recovery path, and failing a 25-minute bringup on a
release doing what it is configured to do is how a guard ends up
switched off; that the configuration is itself a defect is filed
separately and is not this guard's to assert.
Both probes keep their exit status and neither folds kubectl's stderr
into the value it captures. A read that failed and a release with no
history both print nothing on stdout, so folding those together would
let an API timeout pass for the latter; and kubectl writes warnings to
stderr while exiting 0, which inside the history capture turns an empty
history into a populated one, and inside the strategy capture stops the
value matching RetryOnFailure and downgrades a real teardown to a note.
An empty history is judged against that release's own Ready condition
rather than the parent's: the parent carries DisableWait on both
actions, from the annotation its ApplicationDefinition sets, so it
reaches Ready without waiting for anything it applied, and the suite
waits on some addons by name while never naming others. Ready with no
history is a status shape the helper can no longer read and fails the
run; not-Ready with no history is a release that never completed an
action, which is reported as not inspected rather than failed or passed
over in silence.
Every addon is inspected even after one of them fails, because the notes
the others print - a rollback here, a teardown the default strategy
performed there - are the context that explains the failure, and
stopping at the first one hides what a reader would compare it against.
It adds no wait and no retry, and both calls run before the
tenant-snapshot trap is disarmed, so a teardown either one finds still
captures the tenant cluster. A test pins that wiring by reading the
script: with the guards behind functions, deleting the two calls or
moving them past the trap would otherwise leave every case in the file
green while the run asserted nothing.
Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
Copy file name to clipboardExpand all lines: docs/agents/e2e-testing.md
+8-4Lines changed: 8 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,11 +58,15 @@ The install test fails if **any** HelmRelease is not Ready. A toothless gate (ba
58
58
- Use a single `kubectl wait hr --all -A`, then an outcome-based re-list (covers HRs created after the snapshot), and `exit 1` on any non-Ready HR.
59
59
- Dump the full Ready-condition message per non-Ready HR so the real error is in the test output. See `hack/e2e-install-cozystack.bats`.
60
60
61
-
### 6. Assert the parent HelmRelease did not remediate — via `status.history`
61
+
### 6. Assert no HelmRelease was torn down against its own configuration — via `status.history`
62
62
63
-
A parent HelmRelease that hit its wait timeout, uninstalled, and reinstalled is a silent race we want to catch. **Do not** check `.status.installFailures` / `.status.upgradeFailures`: Flux's `ClearFailures()` zeroes those on every successful reconcile, so checking them after the HR is Ready is **vacuous** and passes against a reverted fix.
63
+
A HelmRelease that hit its wait timeout, uninstalled, and reinstalled is a silent race we want to catch — and on the releases that no longer recover that way, it is a race with no explanation inside the release at all. **Do not** check `.status.installFailures` / `.status.upgradeFailures`: Flux's `ClearFailures()` zeroes those on every successful reconcile, so checking them after the HR is Ready is **vacuous** and passes against a reverted fix.
64
64
65
-
- Inspect `.status.history` instead — a `failed` or `uninstalled` Snapshot survives a later successful reconcile. Use the shared helper in `hack/e2e-chainsaw/_lib/remediation-guard.sh` (`helmrelease_has_remediation_cycle`) from a `script` step.
65
+
- Inspect `.status.history` instead — a `failed` or `uninstalled` Snapshot survives a later successful reconcile. Use the shared helpers in `hack/e2e-chainsaw/_lib/remediation-guard.sh` from a `script` step.
66
+
- **Fail on a teardown the release is configured not to perform; report everything else.** `helmrelease_has_teardown` reports an `uninstalled` Snapshot, which is proof the release was removed and put back. Two configurations write it — the default strategy's install remediation, and an upgrade remediation set to `strategy: uninstall`, which nothing under `packages/` uses — so on a `RetryOnFailure` release with neither — the tenant `cilium` and `csi`, and every Application HelmRelease, which the aggregated apiserver stamps that way (`pkg/registry/apps/application/rest.go`) — an `uninstalled` Snapshot has no explanation inside the release and fails the run. On a release still using the default strategy the same Snapshot is that strategy doing its documented job, and it is printed instead.
67
+
- **Report `failed` rather than failing on it.** `helmrelease_has_remediation_cycle` is the broader `failed`-or-`uninstalled` reading; it catches the upgrade remediation that rolls back rather than uninstalls, but it is not proof anything was removed, since a retried install under `RetryOnFailure` leaves `failed` behind with its manifests still applied.
68
+
- **Do not put an unmeasured pass condition on the gate.** The tenant addons that set no strategy pair the default one with `remediation.retries: -1`, a configuration that declares repeated uninstall-and-reinstall their recovery path. Nothing in the suite measures how often they take it, and failing a 25-minute bringup on a release doing what it is configured to do is how a guard ends up switched off. That the configuration is itself a defect is a separate matter from what the guard asserts. Print the finding, name the release, leave the run green, and tighten when a measured baseline or a strategy change makes the condition knowable.
69
+
- **Do not infer a child release's state from the parent's.** The Kubernetes `ApplicationDefinition` sets `release.cozystack.io/helm-install-disable-wait`, which the apiserver turns into `DisableWait` on both actions, so the parent reaches Ready without waiting for anything it applied — and the suite waits on some addons by name while never naming others. An empty `.status.history` is therefore judged against that release's own Ready condition: Ready with no history is a status shape the helper can no longer read and fails the run, not-Ready with no history is a release that never completed an action and is reported as not inspected.
66
70
67
71
### 7. Test-Impact Analysis (TIA), default-on
68
72
@@ -126,7 +130,7 @@ The suite is pinned to Chainsaw **v0.2.15** (the latest release as of May 2026);
126
130
5. Controller-created artifacts the test cannot reclaim are pruned explicitly; nested tenants delete child → parent with a wait-for-deletion between.
127
131
6. Standard HR-Ready assert timeout is **5–6m**; longer waits (harbor 10m, NFS 10m, VM image pulls, platform-wide install 15m) are justified in-line.
128
132
7. Failure path attaches scoped diagnostics via a `catch:` block, never a silent pass.
129
-
8. If it touches parent-HR behavior, add the `status.history` remediation guard (`hack/e2e-chainsaw/_lib/remediation-guard.sh`).
133
+
8. If it touches HelmRelease install/upgrade behavior, add the `status.history` guard (`hack/e2e-chainsaw/_lib/remediation-guard.sh`): fail the run on a teardown of a `RetryOnFailure` release, report every other footprint without failing. On an Application-served HelmRelease the broad `failed` reading is the only footprint the release's own configuration can produce, and it is informational; an `uninstalled` Snapshot there comes from outside the release and fails the run.
130
134
131
135
## In-flight direction (not yet the merged standard)
echo"Reading the Ready condition of ${_hr} failed - kubectl's error is above.">&2
705
+
return 1
706
+
fi
707
+
if [ "${_ready}"="True" ];then
708
+
echo"Unexpected empty .status.history on Ready HelmRelease ${_hr} - Flux API shape may have changed.">&2
709
+
kubectl -n "$_ns" describe hr "${_hr}">&2
710
+
return 1
711
+
fi
712
+
echo"» ${_hr} is not Ready and has no release history - no completed helm action to judge, not inspected"
713
+
return 0
714
+
fi
715
+
if helmrelease_has_teardown "${_history}";then
716
+
if [ "${_strategy}"="RetryOnFailure" ];then
717
+
echo"HelmRelease ${_hr} was uninstalled and reinstalled, though its install strategy is RetryOnFailure, which does not uninstall to recover. The other configuration that uninstalls is an upgrade remediation with strategy: uninstall - check whether one was added before looking outside the release.">&2
718
+
kubectl -n "$_ns" describe hr "${_hr}">&2
719
+
return 1
720
+
fi
721
+
echo"» NOTE: ${_hr} was uninstalled and reinstalled by its own install remediation, which is what the default strategy does with retries: -1. Not failing the run; read this as the cause if something downstream did fail."
722
+
return 0
723
+
fi
724
+
if helmrelease_has_remediation_cycle "${_history}";then
725
+
echo"» NOTE: ${_hr} carries a failed Snapshot - it was remediated in place and recovered. Not failing the run; read this as the cause if something downstream did fail."
726
+
fi
727
+
return 0
728
+
}
729
+
730
+
# Run that guard over the tenant addon HelmReleases a parent release installs.
731
+
# $1 is the namespace, $2 the name prefix the addons share ("kubernetes-<test>-").
732
+
#
733
+
# The parent's own .status.history says nothing about its children, so a
734
+
# remediation cycle on one of them surfaces only as whatever downstream deadline
735
+
# it eventually breaks -- a node that never turns Ready, a PVC that never binds
736
+
# -- with nothing in the run naming the teardown that preceded it. The CNI and
737
+
# CSI releases are the sharp cases, because their teardown removes a
738
+
# cluster-wide precondition rather than churning one component.
739
+
#
740
+
# The addons are read from the cluster rather than listed here, so a release the
741
+
# chart adds later is covered without touching this script.
742
+
#
743
+
# Adds no wait, and does not assume one happened. The parent HelmRelease carries
744
+
# DisableWait on both actions (the Kubernetes ApplicationDefinition sets
745
+
# release.cozystack.io/helm-install-disable-wait, deliberately, because its addon
746
+
# releases cannot go Ready before worker nodes exist), so the parent reaching
747
+
# Ready says nothing about them; and the suite waits on some of them by name
748
+
# while never naming others, metrics-server and prometheus-operator-crds among
749
+
# them. That is why each release is judged on what its own object says, readiness
echo"No addon HelmReleases matched ${_prefix}* in ${_ns}. The parent is Ready, so either this prefix no longer selects its addon releases, or the chart stopped rendering them - they are gated on _namespace.etcd in packages/apps/kubernetes/templates/helmreleases/.">&2
764
+
printf'%s\n'"${_all_hrs}">&2
765
+
return 1
766
+
fi
767
+
# Every addon is inspected even after one of them fails. The notes the others
768
+
# print - a rollback here, a teardown the default strategy performed there -
769
+
# are the context that explains the failure, and stopping at the first one
770
+
# hides exactly the releases a reader would compare it against.
0 commit comments