Repository navigation
fix(e2e): fail on a failed release of cilium and csi as on the parent, and report addon remediation - #3594
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe E2E remediation checks distinguish teardown from failed remediation, validate parent and addon HelmReleases, handle API and history states, and keep the failure snapshot trap active during validation. ChangesHelmRelease remediation guards
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant KubernetesTest
participant cozy_guard_all_helmreleases
participant cozy_guard_helmrelease
participant kubectl
KubernetesTest->>cozy_guard_all_helmreleases: validate parent and addon HelmReleases
cozy_guard_all_helmreleases->>cozy_guard_helmrelease: validate each release
cozy_guard_helmrelease->>kubectl: read strategy, history, and Ready state
kubectl-->>cozy_guard_helmrelease: return release state or read error
cozy_guard_helmrelease-->>cozy_guard_all_helmreleases: return release result
cozy_guard_all_helmreleases-->>KubernetesTest: preserve aggregate failures
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Reopening to pick up the multus fix on main. |
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM. Verified statically and hermetically (no cluster): the fix premise checks out against the code — the aggregated apiserver unconditionally stamps RetryOnFailure on every Application HR (rest.go:1601-1611), cilium/csi set it explicitly, and addons without a strategy fall back to the default, which matches the guard's teardown-vs-retry logic. The guard aborts the run for real (set -eu in the chainsaw step, not decorative), there's no local-assignment status masking, and the empty-selection path is fail-closed, not fail-open. 30/30 unit tests pass; non-vacuity confirmed by two mutations.
Non-blocking notes:
- The guard's three kubectl reads are not wrapped in the existing kubectl_wait_retry helper. In the etcd-noisy sandbox a single transient "leader changed" would redden the whole run. Fail-closed is the right default here; wrapping the reads in a bounded retry (distinguishing a transient error from a missing field) would remove a new false-red source.
- Addon-guard coverage is bounded by DisableWait (an addon torn down after the check point won't be seen) — best-effort, already acknowledged.
- grep "^${_prefix}" treats the prefix as a regex; input is controlled so no practical risk, but grep -F would be stricter.
e85e915 to
bf5ad40
Compare
bf5ad40 to
904285f
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
hack/e2e-chainsaw/_lib/run-kubernetes.sh (1)
1659-1659: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd diagnostics to the parent readiness wait.
If the parent HelmRelease does not become Ready in 5m,
kubectl waitfails and errexit ends the run. The only output is kubectl's timeout message. The guard below printsdescribe hron its own failure paths, but this wait prints nothing about the release state, so the stuck release is not named. Every other long wait in this function (node join, backend readiness, scheduling gate) dumps scoped diagnostics beforeexit 1.Note that the tenant snapshot trap captures the tenant cluster, not the management-cluster HelmRelease objects, so it does not cover this gap.
♻️ Proposed diagnostics on the wait path
- kubectl wait hr -n tenant-test "kubernetes-${test_name}" --timeout=5m --for=condition=ready + if ! kubectl wait hr -n tenant-test "kubernetes-${test_name}" --timeout=5m --for=condition=ready; then + echo "=== parent HelmRelease kubernetes-${test_name} did not become Ready within 5m — diagnostics follow ===" >&2 + kubectl -n tenant-test get hr >&2 || true + kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2 || true + exit 1 + fi🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hack/e2e-chainsaw/_lib/run-kubernetes.sh` at line 1659, Update the parent HelmRelease readiness wait around kubectl wait for kubernetes-${test_name} to handle timeout failure explicitly, print scoped diagnostics for that HelmRelease with kubectl describe hr in the relevant namespace, then exit with failure while preserving the existing 5-minute wait and success path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@hack/e2e-chainsaw/_lib/run-kubernetes.sh`:
- Around line 740-751: Source remediation-guard.sh in the runtime path before
the cozy_guard_all_helmreleases helper is invoked. Update
hack/e2e-chainsaw/_lib/run-kubernetes.sh so helmrelease_has_teardown and
helmrelease_has_remediation_cycle are defined before this guard logic executes.
---
Nitpick comments:
In `@hack/e2e-chainsaw/_lib/run-kubernetes.sh`:
- Line 1659: Update the parent HelmRelease readiness wait around kubectl wait
for kubernetes-${test_name} to handle timeout failure explicitly, print scoped
diagnostics for that HelmRelease with kubectl describe hr in the relevant
namespace, then exit with failure while preserving the existing 5-minute wait
and success path.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 525b4c53-2d96-4269-8447-f1b4043c1a48
📒 Files selected for processing (4)
docs/agents/e2e-testing.mdhack/e2e-chainsaw/_lib/remediation-guard.shhack/e2e-chainsaw/_lib/run-kubernetes.shhack/run-kubernetes-remediation_test.bats
🚧 Files skipped from review as they are similar to previous changes (2)
- hack/run-kubernetes-remediation_test.bats
- hack/e2e-chainsaw/_lib/remediation-guard.sh
904285f to
ed56999
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/agents/e2e-testing.md (1)
46-47: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRemove the
EXIT-trap exceptions from this convention.The supplied path instruction requires no
EXITorRETURNtraps. Lines 46-47 and 157 currently permitEXITtraps insidescriptsteps and BATS subshells. Keep cleanup explicit, or update the governing instruction and all related examples together.As per path instructions, use no
EXITorRETURNtraps.Also applies to: 157-157
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/agents/e2e-testing.md` around lines 46 - 47, Update the convention in this document to prohibit all EXIT and RETURN traps, removing the exceptions for single script steps and explicit BATS subshells. Revise the related guidance and examples, including the references to hack/e2e-test-openapi.bats and sourced shell libraries, so cleanup is performed explicitly without either trap type.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/agents/e2e-testing.md`:
- Line 65: Revise the guidance around .status.history to state that
helm-controller/api is only a pinned dependency and does not contain this
repository’s controller behavior for reading or truncating history. Keep claims
about retention and strategy-specific truncation explicitly attributed to the
upstream helm-controller source, while preserving the existing e2e timing and
shared remediation-guard helper guidance.
- Line 66: Update the paragraph describing the helmrelease_has_teardown rule to
match cozy_guard_helmrelease’s implemented behavior: document that an
uninstalled Snapshot is treated as unexplained only when
.spec.install.strategy.name is RetryOnFailure. Remove claims about
remediation.retries or upgrade remediation strategy unless the guard is expanded
to read and handle those fields.
In `@hack/e2e-chainsaw/_lib/run-kubernetes.sh`:
- Around line 1001-1010: Bound every kubectl read in the guard helpers: wrap the
strategy and history reads near lines 1001-1010, the Ready read near lines
1025-1029, and the HelmRelease list read near lines 1103-1107 in timeout -k 5
30, adding --request-timeout=30s to each kubectl invocation.
- Around line 1995-2005: Wrap the parent HelmRelease wait for
“kubernetes-${test_name}” in a failure branch that, when kubectl wait fails,
prints the HelmRelease YAML/status conditions and relevant namespace events,
then exits immediately with failure. Keep the existing 5-minute readiness wait
and ensure the remediation guard does not run after a timeout.
---
Outside diff comments:
In `@docs/agents/e2e-testing.md`:
- Around line 46-47: Update the convention in this document to prohibit all EXIT
and RETURN traps, removing the exceptions for single script steps and explicit
BATS subshells. Revise the related guidance and examples, including the
references to hack/e2e-test-openapi.bats and sourced shell libraries, so cleanup
is performed explicitly without either trap type.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 179fb467-b311-4da5-8636-7508a55bb033
📒 Files selected for processing (2)
docs/agents/e2e-testing.mdhack/e2e-chainsaw/_lib/run-kubernetes.sh
IvanHunters
left a comment
There was a problem hiding this comment.
This PR splits the old single helmrelease_has_remediation_cycle reading into a narrow helmrelease_has_teardown (only an uninstalled Snapshot) and the existing broad reading, and uses .spec.install.strategy.name to decide which one is fatal. It also extends the guard from the parent HelmRelease to the tenant addon HelmReleases via cozy_guard_addon_helmreleases/cozy_guard_all_helmreleases in hack/e2e-chainsaw/_lib/run-kubernetes.sh. I verified the wiring, the ordering guarantee, and the test coverage directly against the PR head. I agree with the design. LGTM with a couple of non-blocking notes.
What I verified
cozy_guard_all_helmreleases tenant-test "kubernetes-${test_name}"is called athack/e2e-chainsaw/_lib/run-kubernetes.sh:2029, and the tenant-snapshottrap - EXITis at line 2048 — the guard genuinely runs before the trap is disarmed, matching the ordering claim in the newhack/run-kubernetes-remediation_test.batstest.helmrelease_has_remediation_cycleinhack/e2e-chainsaw/_lib/remediation-guard.shgains a missinglocal statusesdeclaration alongside the newhelmrelease_has_teardown, closing a real global-variable leak — a genuine correctness fix, not just a comment change.- The new
hack/run-kubernetes-remediation_test.batsfixture-drivenkubectlstub distinguishes "read failed" (*.FAILS) from "field absent" (missing fixture) and from "kubectl warns on stderr but exits 0" (*.WARNS), and there are dedicated tests proving a stray stderr warning doesn't silently downgrade a real teardown or mask an empty history. That's the right level of paranoia for this kind of guard. - The ordering-pin test in
hack/run-kubernetes-serial-console_test.batswas re-anchored fromgrep -n 'helmrelease_has_remediation_cycle "${history_statuses}"'togrep -n '^ *cozy_guard_all_helmreleases ', i.e. on the call site rather than on the callee's now-renamed local variables — this correctly avoids brittleness from the refactor.
Non-blocking / notes
- One of the open CodeRabbit threads (
hack/e2e-chainsaw/_lib/run-kubernetes.sh, originally line 751) claimsremediation-guard.sh"is not sourced from the current repository and is not referenced by run-kubernetes.sh." That's incorrect — line 3 ofrun-kubernetes.shat the very commit CodeRabbit reviewed and at current head both read. hack/e2e-chainsaw/_lib/remediation-guard.sh. No action needed on the code, but worth resolving/dismissing the thread explicitly so it doesn't linger as a false "unaddressed Major finding." - The new guard's kubectl reads (
cozy_guard_helmrelease's strategy/history/Ready reads,cozy_guard_addon_helmreleases's listing) have no--request-timeout/timeoutwrapper, unlike other reads added elsewhere in this file. Both a prior human review and a later CodeRabbit pass flagged this independently: a wedged apiserver at guard time (which runs late, near the tenant-snapshot trap) could stall and, per the guard's own doc comments, cost part of the diagnostic budget. Worth a follow-up. docs/agents/e2e-testing.md's new bullet describes two configurations that can produce anuninstalledSnapshot (default-strategy install remediation, and an upgrade remediation set tostrategy: uninstall), butcozy_guard_helmreleaseonly reads.spec.install.strategy.name— it doesn't check.spec.install.remediation.retriesor.spec.upgrade.remediation.strategy. This gap is honestly self-documented in the code's own comment block ("the rule implemented is narrower than the rule stated..."), and no current addon is in the affected shape (retries: -1everywhere), so it's a real but currently-inert limitation rather than a bug.- The plain
kubectl wait hr -n tenant-test "kubernetes-${test_name}" --timeout=5m --for=condition=readycall (unchanged by this diff, only the surrounding comment was rewritten) still prints nothing but kubectl's own timeout message on failure. CodeRabbit raised this twice; it predates this PR and is arguably out of scope for a remediation-guard fix, but it's a reasonable follow-up since the guard right below it depends on this wait having succeeded. - Per the metadata, this PR is
CONFLICTINGagainstmainand needs a rebase before it can merge — noted as a mergeability item, not a logic concern; nothing in the diff suggests the conflict hides a real semantic clash.
Overall: the core reasoning (RetryOnFailure is stamped on every Application-served HelmRelease, so a failed Snapshot there is not evidence of a teardown) is correctly implemented and heavily unit-tested. The open notes above are worth acting on before or shortly after merge, but none of them constitute a regression or a broken assertion.
8cd6277 to
9ca986f
Compare
myasnikovdaniil
left a comment
There was a problem hiding this comment.
The classification is right and the tests are the best part of this PR. Requesting changes on where the guard is wired in, and on the field that arms it.
On the tests first, because they deserve saying. I mutated 23 ways, gate removed, gate inverted, teardown predicate widened to failed, || true on each of the two reads, stderr folded into each capture, the addon loop swallowing verdicts, the composite dropping either verdict, read order swapped, index() traded for an anchored grep, the call deleted, the call suffixed with || true. Every one turns a test red, each with a case that exists for it. Both suites behave identically under bats and under hack/cozytest.sh, 24 of 24 and 12 of 12. Every repo fact the change rests on holds too: the parent gets RetryOnFailure on both install and upgrade, cilium and csi set it, all 19 addons set retries: -1, nothing under packages/ sets an uninstalling upgrade remediation, and both truncation variants behave as the comments say.
The guard cannot run in the case it was written for. Its own comment says a child remediation surfaces only as whatever downstream deadline it eventually breaks, a node that never turns Ready, a PVC that never binds, with nothing in the run naming the teardown that preceded it. The call is at run-kubernetes.sh:2520 and every one of those deadlines is upstream of it, the node-Ready wait at :1942 with exit 1 at :1954, the csi HelmRelease wait at :2156, then the LB and NFS checks. Under set -eu the script ends there.
This PR's own CI is the demonstration. Tenant cilium never went Ready, coredns, csi, ingress-nginx, metrics-server and ouroboros all sat on dependency '...-cilium' is not ready, the run died at the node-join deadline, and cozy_guard_all_helmreleases was never reached. Had cilium been torn down instead of retried the output would have been identical and the teardown still unnamed. cozy_report_node_join_failure prints each HelmRelease's Ready message and not .status.history, so the failing path is exactly as blind as before this PR. Calling a print-only classification from cozy_report_node_join_failure, or from _tenant_snapshot_on_fail at :1915, closes it. If the happy-path-only placement is deliberate then say so in the comment and in the docs bullet, because both currently claim more.
The field that arms the guard fails soft and nothing pins it. The fatal verdict is a string equality against a value read by kubectl -o jsonpath, and with --allow-missing-template-keys at its default a path that stops resolving prints nothing and exits 0, so empty takes the soft branch. That is the same class of silent failure the empty-history check exists to catch, applied to .status.history and not to the new read. Bump helm-controller/api past a rename of spec.install.strategy and every teardown, including a real removal of the tenant CNI, becomes a NOTE about its own install remediation, green run, all 24 unit tests still passing because they feed the strategy from a fixture rather than through the jsonpath. The printed evidence does not help either, (install strategy <unset>) is the expected line for 17 of the 19 addons. Worth knowing the existing pin does not cover this shape, remediation-guard.bats:143 pins .status.history[].status against a hand-written YAML fixture rather than against the module in go.mod, so the docs claim that the shape is pinned against helm-controller/api v2 is stronger than what that test does. Cheapest fix is a canary, the parent is RetryOnFailure unconditionally so an empty strategy on the parent should be fatal.
Smaller things, none blocking alone. The retries: 0 gap you document at :1596-1605 is one more field in the same jsonpath, appending @{.spec.install.remediation.retries} and treating either condition as fatal closes it and deletes ten lines explaining why it is open. The enumerate-from-the-cluster design was chosen precisely so a release added later is covered without editing this file, and that is the shape where it does not hold. The read-order argument at :1543-1546 says no truncation variant can empty a non-empty history, which is true, but the controller also calls Status.ClearHistory() outright on an unmanaged release, so if that lands between the two reads the guard sees Ready true and empty history and fails the run with the wrong explanation. Tiny window, but the claim should be the narrower one. And it is worth naming in the docs that three releases out of twenty can actually fail the run on history content, since for the other seventeen every history shape is a note. That is a defensible design, it just is not what covering the tenant addon HelmReleases reads as.
On the two failure directions. A real failure read as a fine teardown is reachable and you relaxed it deliberately, failed is now a note everywhere, and the justification is correct against the controller since an active retry returns an upgrade rather than uninstall remediation. But this run shows the limit of recovered by design, cilium logged UpgradeFailed ... stalled resources: [Deployment/cozy-cilium/cilium-operator status: 'Failed'] eleven times over ten minutes and never recovered, and in that state the guard prints its failed-Snapshot note and returns 0. Right call for this guard, the run is already red, but retried and wedged in a retry loop are the same output and the wording is the only difference. The opposite direction is effectively unreachable, a release pairing RetryOnFailure with an uninstalling upgrade remediation would trip the fatal branch on a removal it is configured to perform, and nothing under packages/ does that.
The red E2E is not yours. Both suites fail at the node-join deadline behind tenant cilium stuck on cilium-operator, #3780 which only moves documentation fails identically, #3790 shows the same cilium signature, and nightlies have been red seven days running. Everything this PR touches in the run lives below the line where those runs die, which is also why this code has never executed on a cluster, and why the placement point above matters more than it otherwise would.
| # cozy_guard_all_helmreleases judges the parent and the addons and reports | ||
| # both verdicts rather than stopping at the parent's; see its comment for why | ||
| # that matters under errexit. | ||
| cozy_guard_all_helmreleases tenant-test "kubernetes-${test_name}" |
There was a problem hiding this comment.
This is the placement I am blocking on. The function's own comment says a child remediation surfaces only as whatever downstream deadline it eventually breaks, a node that never turns Ready, a PVC that never binds. All of those are upstream of this line: the node-Ready wait at :1942 with its exit 1 at :1954, the csi HelmRelease wait at :2156, then LB and NFS. Under set -eu the script ends at the first of them.
So on this PR's own run the guard never executed. Tenant cilium never reached Ready, the addons sat on dependency '...-cilium' is not ready, and the run died at the node-join deadline. A teardown would have produced the same output and still gone unnamed, because cozy_report_node_join_failure prints Ready messages and not .status.history.
A print-only classification called from cozy_report_node_join_failure, or from _tenant_snapshot_on_fail at :1915, would give the failing path the coverage the comment promises. If happy-path-only is deliberate, the comment and the docs bullet should say so.
There was a problem hiding this comment.
You are right, and it took two changes. The print-only classification you suggested is cozy_report_helmrelease_remediation, called from cozy_report_node_join_failure and from the EXIT handler that covers every other failure. It reuses the guard's own enumeration rather than a second copy of the selection, so the two readings cannot drift apart.
The second change your comment led me to and I had missed. The EXIT handler was armed only once the tenant LB answered, so even with the collector in place it would have been absent from the whole stretch between the CR apply and that point, which is management-cluster waits on the parent HelmRelease and on what it installs. That stretch is the window the guard exists for: a helm-wait budget expiring while admin-kubeconfig is still being provisioned is what triggers the uninstall remediation it reads. It arms at the apply now, in 91cbb69, and the snapshot half stays a no-op until the tenant kubeconfig exists.
Inside the node-join collector the classification runs after the serial console rather than before it (98f7d8c). The console is the only artefact covering a worker that never reached apid, and its worst case takes most of the phase budget, so anything ahead of it competes with it on the slow runs it exists for. What the classification reads outlives the run and can be read again afterwards; the console cannot. Both walks are bounded by a request timeout and a wall clock checked before the listing and before each release, and a release the budget cut off is named rather than dropped.
Head is 98f7d8c, rebased onto current main. The classification, its budget, the arming point, the collector order and the wiring are pinned in hack/run-kubernetes-remediation_test.bats and in the two order guards in hack/run-kubernetes-node-join_test.bats.
There was a problem hiding this comment.
Rebased since my last reply, so the line numbers in your comment and the SHAs in mine no longer resolve. Current positions on the head you would read now.
The EXIT handler arms at 5357, right after the tenant CR apply, instead of once the tenant LB answers. Everything you named sits below that point: the node-Ready wait and its failure path at 5523-5540, the csi HelmRelease wait at 5789. Under errexit each of them now enters _tenant_failure_on_exit rather than ending the script above the classification.
cozy_report_helmrelease_remediation is at 5122 and is reached from two places: line 412 inside the EXIT handler, ahead of the tenant snapshot, and line 4422 inside cozy_report_node_join_failure, after the serial console capture at 4398. Print-only holds the way the comment promises: every cozy_guard_helmrelease call inside it is || true, it returns 0 on all paths including the budget cut-offs, and both call sites are || true as well.
Your 1942, 1954 and 2156 are from before the rebase and the file was rewritten around them.
There was a problem hiding this comment.
Rebased onto current main. The file was rewritten under the branch, so the line numbers in my earlier reply no longer point anywhere.
The wiring you blocked on is pinned by tests now. cozy_report_helmrelease_remediation is reached from cozy_report_node_join_failure and from the EXIT handler, and the handler is armed at the tenant CR apply, above every wait you named.
Nothing covered the assignment itself. The handler classifies whatever CURRENT_TENANT_PARENT_HR holds, so deleting that one line leaves it empty: the collector returns on its empty-parent gate, every failure path goes quiet, and the suite stays green. There is a test for that now, and for the assignment landing before the arming.
There was a problem hiding this comment.
myasnikovdaniil everything from your review is addressed at the current head, so could you take another look? The branch is two commits now: the code is in 8cb1023 and the docs in b42805b. Line numbers below are at that head.
- The exit handler
_tenant_failure_on_exitis armed right after the tenant CR apply (run-kubernetes.sh:5798), above every wait you listed. It calls the print-onlycozy_report_helmrelease_remediation(:771), and so does the node-join collector (:4833). failedis fatal again on everyRetryOnFailurerelease, which means the parent, cilium and csi.- An empty or non-
RetryOnFailurestrategy on the parent fails the run (:5293and:5301). - The ClearHistory window is described as open (
:5254). - The docs point at
internal/fluxcontractfor the history jsonpath pin, and they list which conditions can fail a run.
The retries: 0 gap went away with the retry-budget branch. A default-strategy addon gets a note whatever its retries value. One thing is still open: an addon added later with retries: 0 would also get a note, and nothing in this PR checks that every addon sets -1.
The guard bats suites and internal/fluxcontract pass at this head.
|
Re-read this against head while my block was standing, and found one thing I missed the first time that I think changes the approach rather than the diff. The condition this makes fatal cannot happen. In helm-controller v1.5.1, Two smaller things while I was in there. The guard call still sits after every failure exit in the file, so on the failure path its own comment names as the motivation it does not run, which is the block I already have on that line and it is unaddressed at head. And the new reads at The discriminator itself is fine, for the record. |
4e822f1 to
f6262a6
Compare
|
myasnikovdaniil I reworked the PR around your objection. On a At the new head a |
|
IvanHunters Your 09-03 notes are handled at the new head, after a rebase onto main. The runner-canary pin now anchors on I left the reads unretried. The rule itself changed after myasnikovdaniil's objection. |
a00d3ce to
2294faf
Compare
|
myasnikovdaniil the placement you blocked on is fixed and pinned, and the empty-strategy canary on the parent and the narrower ClearHistory claim are in. 2294faf is rebased onto current main, with only a comment clarified. Could you take another look? E2E is skipped right now because |
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM
The teardown-vs-retry split now fails the run on a failed or uninstalled Snapshot of a RetryOnFailure release (the parent, cilium, csi) and only notes it on the default-strategy addons, which is the opposite of the fail-soft-on-failed shape the earlier objection was raised against. I reproduced the central guarantee by mutation: flipping the fatal branch to return 0 reddens 8 tests in hack/run-kubernetes-remediation_test.bats. The fatal-path tests assert failure in the positive form, so they are not among the negated assertions cozytest.sh swallows. Reviewed hermetically at head 2294fafa1, no cluster. One correctness limitation below is worth fixing but does not hold the review (a test-harness false-negative, not a product or data risk), and the code already improves on main.
Two things gate the merge and neither is in the diff. First, myasnikovdaniil's CHANGES_REQUESTED is still open on the PR; the rework answers its substance (a failed Snapshot on a RetryOnFailure release is now fatal, not soft), but the review has not been dismissed or re-submitted, so a human block stands until that reviewer looks again. Second, the Run unit and controller tests check (job 110989675196) is red on this head and it is not this PR: internal/controller/workloadmonitor_controller_test.go fails to build against sigs.k8s.io/controller-runtime v0.25.1, whose controllertest.FakeInformer has Synced as a method and no SyncedLock field, while the test assigns fi.Synced = true and locks fi.SyncedLock; that file, the fake and the go.mod pin are byte-identical at the merge-base and untouched here, and the PR's own Go test (internal/fluxcontract) builds and passes. The breakage is on main itself and it is what keeps E2E from running at this head.
Findings
- [MINOR]
hack/e2e-chainsaw/_lib/run-kubernetes.sh:5191, A single failed install that recovers on one retry leaves no failed Snapshot, so the guard passes on exactly the race it targets
- [NIT]
hack/e2e-chainsaw/_lib/run-kubernetes.sh:5290, Ready-condition jsonpath rename silently retires the empty-history-on-Ready canary
- [MINOR]
hack/e2e-chainsaw/_lib/run-kubernetes.sh:5243, Guard reads are fail-closed and unretried: one transient apiserver error on any release reds the run
Still open from my earlier rounds
The Ready-condition half of the empty-history canary is still soft. The parent now fails the run on an empty .spec.install.strategy.name, which closes the strategy-rename half; a rename of the Ready-condition jsonpath is not covered, because _ready would read empty everywhere, a Ready release with an empty history would take the "not Ready ... not inspected" branch and pass, and the canary would be off with nothing to notice. Latent and second-order (a release with a real teardown carries a non-empty history), so it does not hold the review.
Checked and correct
The guard runs before the EXIT trap is disarmed, so a fatal verdict on the passing path still triggers the tenant snapshot, an ordering pinned by the re-anchored serial-console test. _COZY_GUARD_REPORT_DONE dedups the classification across all three call sites (guard, node-join collector, exit handler). The trap is armed at the CR apply and the snapshot half no-ops until CURRENT_TENANT_KC is set, so a wait that times out before the tenant is reachable still gets its history printed. Reads are bounded (timeout -k + --request-timeout), a cut-off read keeps its non-zero status rather than being folded into "no history", and stderr is kept out of the captures. The stale denominators from the earlier comments are gone and the runner-canary pin is re-anchored on CURRENT_TENANT_KC=.
| # tenant cilium and csi, whose templates set it on both actions. Under that | ||
| # strategy the controller never remediates: a failed action is retried as an | ||
| # upgrade with its manifests left in place, so what it leaves behind is a | ||
| # "failed" Snapshot, and that Snapshot is the only record the release keeps of |
There was a problem hiding this comment.
[MINOR] A single failed install that recovers on one retry leaves no failed Snapshot, so the guard passes on exactly the race it targets
The guard matches only failed/uninstalled (helmrelease_has_remediation_cycle, :5308). But under RetryOnFailure a failed install retried as a successful upgrade is recorded superseded, not failed: Helm v4.1.1 picks the failed release as originalRelease when no deployed release exists (pkg/action/upgrade.go:258-262) and rewrites it failed->superseded on success (:511-512), and helm-controller v1.5.1 recordOnObject replaces the prior failed snapshot in .status.history with that observation (internal/reconcile/release.go:96-110). History ends [deployed, superseded] and the guard passes green. The repo's own green test pins coredns history deployed/superseded as clean (:311). A failed entry survives only after >=2 consecutive failed attempts. The single install-timeout-then-recover case is the headline scenario this guard cites, and the claim here (:5191-5193) and in docs/agents/e2e-testing.md:92 that the failed Snapshot is the only record and a recovered retry still fails the run is therefore false. The predicate gap is pre-existing (merge-base used the same match at :6085); the overstated claim is new. Harness false-negative, non-blocking: either strengthen detection (a recovered RetryOnFailure release leaves >1 history entry where the suite ran no upgrade of its own) or correct the two claims to state the real limitation.
There was a problem hiding this comment.
Confirmed at helm-controller v1.5.0 / Helm v4.1.1: with no deployed release, the retry upgrades from the failed install, upgrade.go:258-262 and :511-512 mark it superseded, and recordOnObject (release.go:96-110) rewrites that Snapshot in place, so the history reads deployed, superseded and the guard passes. A failed upgrade keeps its failed Snapshot, since Helm supersedes the last deployed release instead, and so does an install that fails twice, so the gap is exactly the one-failed-install-then-recover case.
I took the second option: 8cb1023 corrects the comment in cozy_guard_helmrelease and in remediation-guard.sh, fixes the RetryOnFailure test fixture to the history a real recovery leaves (deployed, superseded, failed), and adds a test pinning the blind spot as a pass so the prose cannot drift back. b42805b corrects the e2e-testing.md bullet and both commit messages drop the claim.
I did not strengthen detection here. "More than one entry" would false-red the OIDC suite, which upgrades the parent on purpose at run-kubernetes.sh:6604 before the guard runs at :6617, so this needs a signal beyond the statuses. I'm filing it separately together with the same overstatement at e2e-testing.md:77, which predates this PR.
| _cozy_guard_kubectl -n "$_ns" describe hr "${_hr}" >&2 | ||
| return 1 | ||
| fi | ||
| # An empty history is read against this release's own readiness. A Ready |
There was a problem hiding this comment.
[NIT] Ready-condition jsonpath rename silently retires the empty-history-on-Ready canary
The parent now fails the run on an empty .spec.install.strategy.name, closing the strategy-rename half of this. The Ready-condition jsonpath is still read soft: a rename in a future helm-controller empties _ready on every release, a Ready release with empty history takes the not-Ready/not-inspected branch and passes, and the canary is off with nothing to notice. Latent and second-order (a real teardown carries non-empty history), so non-blocking. Carried from the 2026-09-03 round.
There was a problem hiding this comment.
I don't think the canary can go dark this way on the passing path. The guard at run-kubernetes.sh:6617 runs after an unconditional kubectl wait hr ... --for=condition=ready on the parent at :6592, under errexit, and after the same wait on csi (:6239) and the waited addons (:6365-6375). Those waits read the same .status.conditions[type=Ready] the probe at :5261 reads. If that condition is renamed, those waits time out and the run goes red before the guard is reached. If only the history path is renamed, _ready still reads True and the canary fires. Both have to move at once for the scenario you describe, and the first of them already reds the run, so I left this unchanged.
| # Snapshot, and neither truncation variant in helm-controller/api empties a | ||
| # non-empty history - Truncate returns early below two Snapshots, | ||
| # TruncateIgnoringPreviousSnapshots cuts only above five. | ||
| # |
There was a problem hiding this comment.
[MINOR] Guard reads are fail-closed and unretried: one transient apiserver error on any release reds the run
Each release costs a listing plus up to three bounded reads, each reddening the run on a non-zero exit, and nothing is retried. The reads are now bounded so a wedged apiserver cannot eat the tenant snapshot, and fail-closed is deliberate (a cut-off read must not pass for no-history). Flagging the decision, not asking for a retry: a transient blip on any guard read is a red run, and the suite's dominant documented flake is a degraded management apiserver. Non-blocking.
There was a problem hiding this comment.
Deliberate, and I'd keep it for now. kubectl_wait_retry (run-kubernetes.sh:63) exists because a kubectl wait holds a watch for minutes and dies on the first server error anywhere in that window. Each guard read is one bounded GET, exposed for a single round-trip, the same as the unretried single reads elsewhere in run_kubernetes_test, such as the admin-kubeconfig fetch at :5847 and the LB IP read at :5884. The guard adds two of those per release to a passing path that already makes many. If a red run ever shows a guard read lost to one of the signatures that helper allowlists, wrapping the reads with the same allowlist is a small change that keeps fail-closed intact, since an exhausted retry still returns non-zero. Until then I'd rather not add a retry with no measured need for it.
Pull request was closed
fe15946 to
fa7706a
Compare
The history guard covered only the parent Kubernetes HelmRelease. A failed action on one of the addon releases it installs left no mark the suite inspected, so a run surfaced only the downstream symptom, typically the node-join deadline, with nothing naming the release behind it. The tenant CNI and CSI are the sharp cases, since they carry a cluster-wide precondition. The guard now reads every addon release of the parent, selected from the cluster by name prefix so a release added later is covered too. A "failed" or "uninstalled" Snapshot fails the run on a release whose install strategy is RetryOnFailure: the parent, which the aggregated apiserver stamps that way, and cilium and csi. The same footprint on the other addons, which pair the default strategy with retries: -1, is printed as a note and leaves the run green. The footprint is not split into a teardown and a retried failure. In helm-controller v1.5.0, the version the platform installs, and unchanged in v1.5.1, a release with an active retry strategy never remediates: atomic_release.go returns an upgrade for a failed release before it reaches the remediation switch (lines 465-475), and the two other paths that uninstall, a changed release target and deletion, both call ClearHistory afterwards. On a RetryOnFailure release an "uninstalled" Snapshot is therefore unreachable, and the "failed" one is the only signal. A retry that later recovered still fails the run, by design, with one blind spot the statuses cannot close: a failed install that the very next attempt recovers is marked superseded by Helm, because there is no deployed release to supersede instead, and its history reads like an ordinary upgrade. The guard runs after the assertions, while every deadline a failed addon breaks ends the script earlier, so the same classification is also printed without a verdict from the node-join collector and from the exit handler. That handler is now armed at the tenant CR apply, so the management-cluster waits before the tenant answers are covered too. Every read is bounded and never folded into "no history", and the parent must read back RetryOnFailure: an empty value there means the jsonpath stopped resolving, and any other value means the apiserver stamp is gone, either of which would turn its footprint into a note. The history read takes its jsonpath from HELMRELEASE_HISTORY_JSONPATH, and the upstream-type pin accepts the bounded wrapper the read goes through. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
The conventions still described a guard on the parent release alone. They now state which footprint fails a run and on which releases, why a footprint on a RetryOnFailure release is fatal even after a retry recovered it and which recovery leaves no footprint at all, why the default-strategy addons only get a note, how the strategy read is kept from failing soft, and why the failure paths carry the same classification without a verdict. Assisted-by: LLM Signed-off-by: Aleksei Sviridkin <[email protected]>
fa7706a to
b42805b
Compare
|
IvanHunters the recovered-install gap is real. I corrected the claim in the code, the docs and both commit messages (8cb1023, b42805b) rather than widening detection in this PR, and the thread says why. Detection is #4736. The other two threads stay as they are, with the mechanism in each. The unguarded pool release is #4737. |
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
LGTM
The rework since my last pass is a claim correction, not a behaviour change, plus one hardening branch, and it lands well. Reviewed hermetically at head b42805bb, no cluster.
Last round I left this at LGTM with three non-blocking notes and flagged two gates that were not in the diff: a red Unit & controller tests and an open block from another reviewer. The CI gate is gone. #4729 merged on 2026-10-02 and the rebase onto current main picked it up, so Unit & controller tests is green and E2E (in-tree) actually ran and passed on this head rather than being skipped behind a red main.
What changed, and how my earlier notes landed:
- The recovered-install gap I raised is now stated as a limitation rather than denied.
cozy_guard_helmrelease,remediation-guard.sh,docs/agents/e2e-testing.mdand both commit messages drop the "a recovered retry still fails" overstatement; theRetryOnFailurefixture moves to the history a real double-failure-then-recover leaves (deployed,superseded,failed), and a new test pins the one-failure-then-recover blind spot as a deliberate pass. Not widening detection here is the right call: "more than one history entry" would false-red the OIDC suite, which upgrades the parent on purpose before the guard runs. Tracked in#4736. - The strategy canary is tighter. On the parent, any
.spec.install.strategy.nameother thanRetryOnFailurenow fails the run, not only an empty one, so a future rename ofspec.install.strategycan no longer route every footprint to the note branch on a green run. I mutated the branch toreturn 0and the new parent-strategy test goes red, so the assertion is live and not one of the negated shapes the runner swallows.
I confirmed the tests are real by mutation: neutralising the new parent-strategy branch reds the parent guard fails when its install strategy is anything but RetryOnFailure, and dropping the node-join pointer from the cilium message reds a failed cilium install is not blamed on the chart without naming a late node join. All 44 cases in hack/run-kubernetes-remediation_test.bats pass at head.
The Ready-condition canary note from before does not hold up as a risk: the unconditional kubectl wait hr --for=condition=ready on the parent at run-kubernetes.sh:6592, under set -eu, reds the run before the guard is reached if that condition is renamed, so both the probe jsonpath and the wait condition would have to move at once and the first of them fails first. Leaving it unchanged is fine.
One merge gate is still outside the diff: myasnikovdaniil's CHANGES_REQUESTED is unresolved on the PR. The substance of it is addressed (a failed Snapshot on a RetryOnFailure release is now fatal), but the review has not been dismissed or re-submitted, so a human block stands until that reviewer looks again.
Findings
- [MINOR]
hack/e2e-chainsaw/_lib/run-kubernetes.sh:5190, Single-release guard cannot see a failed install the next retry recovers
- [MINOR]
hack/e2e-chainsaw/_lib/run-kubernetes.sh:5251, Guard reads are fail-closed and unretried
- [MINOR]
hack/e2e-chainsaw/_lib/run-kubernetes.sh:5449, Guard does not read the KubernetesNodes pool release
| # tenant cilium and csi, whose templates set it on both actions. Under that | ||
| # strategy the controller never remediates: a failed action is retried as an | ||
| # upgrade with its manifests left in place, so what it leaves behind is a | ||
| # "failed" Snapshot, and that Snapshot is the only record the release keeps of |
There was a problem hiding this comment.
[MINOR] Single-release guard cannot see a failed install the next retry recovers
A failed install with no prior deployed release recovers as an upgrade from the failed release, which Helm marks superseded and helm-controller rewrites in place, so the history reads deployed, superseded with no failed Snapshot left for this guard to read. This round documents the limitation and pins it with a test instead of widening detection, which is the right call here: a count-based heuristic would false-red the OIDC suite, which upgrades the parent on purpose before the guard runs. Tracked in #4736. Non-blocking.
| # Snapshot, and neither truncation variant in helm-controller/api empties a | ||
| # non-empty history - Truncate returns early below two Snapshots, | ||
| # TruncateIgnoringPreviousSnapshots cuts only above five. | ||
| # |
There was a problem hiding this comment.
[MINOR] Guard reads are fail-closed and unretried
Each release costs a listing plus bounded reads, and any non-zero exit reds the whole run, so one transient apiserver error on any release is a red run. Deliberate and fail-safe, and consistent with the other single-GET reads in this file (admin-kubeconfig, LB IP). If a red run ever shows a read lost to a known transient signature, wrapping these in the existing retry helper keeps fail-closed intact. Non-blocking.
There was a problem hiding this comment.
Leaving them unretried for now. If a red run shows a guard read lost to one of the signatures kubectl_wait_retry allowlists, I'll wrap the reads then.
| # addons are not, because most of them legitimately set none. See the branch | ||
| # in cozy_guard_helmrelease that reads this argument. | ||
| cozy_guard_helmrelease "$_ns" "${_parent}" required || _failed=1 | ||
| cozy_guard_addon_helmreleases "$_ns" "${_parent}-" || _failed=1 |
There was a problem hiding this comment.
[MINOR] Guard does not read the KubernetesNodes pool release
The guard selects addon releases by name prefix off the parent, which does not cover the KubernetesNodes pool HelmRelease. Tracked in #4737. This is a coverage gap, not a wrong verdict on what it does read. Non-blocking.
There was a problem hiding this comment.
That stays with #4737. The guard reads only the releases the parent prefix selects.
myasnikovdaniil
left a comment
There was a problem hiding this comment.
Approving, both blockers from my previous review are fixed. I checked wiring by mutation: removing trap at CR apply, report call in node-join collector, CURRENT_TENANT_PARENT_HR assignment, or running snapshot before classification makes run-kubernetes-remediation_test.bats red, same for empty and non-RetryOnFailure strategy branches on parent. Out of 30 mutants on code added since my last review 26 are caught.
Guard also ran on real cluster now, E2E on this head read parent and all addon releases in both suites and found nothing.
Not blocking: phase budget gates in cozy_report_helmrelease_remediation are not tested. No test sets _COZY_DIAG_PHASE_DEADLINE, so cozy_diag_phase_has_time always says yes, and deleting per-release gate at run-kubernetes.sh:5550 keeps everything green. Without it walk on node-join path can go past spent phase for its own 90s plus already started reads, and that time is taken from tenant snapshot. One case with deadline in the past that checks decline note and no further reads would cover all three gates. Smaller ones of same shape: required on parent call at :5542 and _cozy_guard_read_bounds at :5237 can be dropped and no test notices.
#4751) ## What this PR does Unit tests on main are red, and that blocks E2E on every open PR. Three contracts broke the same way. In each case a check from one PR and a line from another merged a few minutes apart on 2026-10-05, with no textual conflict. Any one of them alone keeps the job red, so they are fixed together. The first is the pin on how a suite reads HelmRelease history. #4689 added it. It accepts `$(kubectl ...` and, since #3594, the bounded `$(_cozy_guard_kubectl ...` wrapper. Nothing in front of kubectl is allowed. The monitoring suite from #4678 bounds that read with `$(timeout 30 kubectl ...`, and the bound is wanted. So the pin now accepts an optional `timeout [-k N] N` in front of kubectl. The prefix is spelled out on purpose. Any text would also let through `$(echo kubectl ...)`, which names the shared expansion without running it, and that form is now a refuse case. I checked it with mutations: the new accept cases fail against the old pattern, and a pin that takes any prefix fails a refuse case. The second is the flux install in workflows. #4645 forbids running fluxcd.io/install.sh and moved every workflow to the SHA-pinned fluxcd/flux2 action. The isp-hosted lane from #4712 still ran the script. It now uses the same action and version. The third is the negated-assertion audit. It reads the find operand in `\( ! -type d` in select-e2e_test.bats as a shell negation and fails. The line now says `-not -type d`. This is the same change as #4748, so this PR supersedes it. The audit bug itself is #4752. After the Go fix, the full log of the unit job had only these two bats failures left. Closes #4750 Closes #4755 ### Screenshots ### Downstream repositories - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: - [ ] [cozystack/community](https://github.com/cozystack/community) - follow-up: ### Release note ```release-note fix(ci): fix three unit-test contracts that went red on main after adjacent merges. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * History checks now recognize direct `kubectl` reads and reads through the supported wrapper, including timeout-bounded commands with an optional kill-grace setting. Commands that only include `kubectl` as an argument, use similarly named commands, or place a timeout before the wrapper are not treated as reads. * **Chores** * The hosted end-to-end workflow now uses a pinned Flux version, and suite selection handles directory exclusions consistently. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
The history guard in the tenant Kubernetes e2e suite checked only the parent
kubernetes-<test>HelmRelease. A failed action on one of the addon releases it installs left nothing the suite looked at, so a run showed only the downstream symptom, usually the node-join deadline, and never named the release behind it.The guard now reads every addon release of the parent. It selects them from the cluster by name prefix, so a release added to the chart later is covered too. The rule:
failedoruninstalledSnapshot fails the run on any release whose install strategy isRetryOnFailure. That is the parent (the aggregated apiserver stamps it on every Application HelmRelease) and the tenantciliumandcsi.retries: -1, is printed as a note and the run stays green. For the CRD addons that configuration is tracked in Tenant addon CRD HelmReleases remediate by uninstall, and their CRDs carry no resource-policy: keep #3555.A transient
ciliumorcsifailure that later recovers on a retry still turns the run red, by design, as long as its Snapshot still readsfailed. One recovery leaves no such Snapshot: when an install fails and the very next attempt succeeds, Helm marks the failed installsuperseded, because there is no deployed release to supersede, and the history reads like an ordinary upgrade. The guard cannot see that case from the statuses alone. The code and the docs now say so, a test pins it, and detection is left to a follow-up.An earlier revision of this PR tried to tell a teardown apart from a retried failure and made only
uninstalledfatal. That split does not hold. In helm-controller v1.5.0, the version the platform installs, a release with an active retry strategy never remediates.atomic_release.goreturns an upgrade for a failed release before it reaches the remediation switch, and the two other paths that uninstall (a changed release target, and deletion) both clear history afterwards. So on these releasesuninstallednever shows up, andfailedis the only signal. The parent keeps the rule it already had on main, andciliumandcsinow follow it too.The failure paths get the same reading. The guard runs after the assertions, while the deadlines a failed addon breaks end the script earlier, so the same classification is also printed, without a verdict, from the node-join collector and from the exit handler. That handler is now armed at the tenant CR apply, so the management-cluster waits before the tenant answers are covered too. Smaller changes that came with this:
HELMRELEASE_HISTORY_JSONPATH, and the upstream-type pin ininternal/fluxcontractnow accepts the bounded wrapper the read goes through.docs/agents/e2e-testing.md§6 and checklist item 8 describe the new rule.On related issues: this adds the
localthat #3774 reports missing inhelmrelease_has_remediation_cycle, but not the sweep over the other_lib/helpers that the issue also asks for. For #3773 it rewrites the guard comment the issue quotes. The waits without a backstop are not touched.Closes #3553.
Screenshots
Not a UI change.
Downstream repositories
I walked the trigger map against the diff. It touches
hack/e2e-chainsaw/_lib/,hack/*.bats, one Go test underinternal/fluxcontract, two spend-order comments in the kubernetes suites, anddocs/agents/e2e-testing.md. No package, schema, CRD, make target or user-facing document changes.Release note