fix(e2e): collect diagnostics for unready pods and record every truncated capture - #3567
Conversation
A `trap ... EXIT` inside an `@test` replaces the one the bats binary installs for its own bookkeeping, so a test that then fails prints no TAP line at all. Not `not ok`, nothing: the run ends with a warning that N of M tests executed, and anyone judging it by grepping for `not ok` reads a green suite. Cleanup moves to the last line of each body instead. Both runners set -e, so a failed test leaves its scratch directory behind, which is what you want to look at anyway. hack/promote-retag_test.bats was failing this way already, reporting five passes and no failures while six of its eleven tests printed nothing. Those six invoke the script for real, and it requires sha256sum; they pin PATH to the stub directory plus /usr/bin and /bin, which is where coreutils installs that binary and where macOS does not, so the failure was of the platform rather than of the code and CI never saw it. The stub directory already supplied yq and skopeo for the same reason and now supplies sha256sum too, resolved from absolute candidates because the stub itself is first on PATH. Two file headers described the trap arrangement they no longer use and are corrected rather than left to outlive it. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
The capture wrote its reasons to the job log only. A reader holding the uploaded artifact saw N logs in a directory with no way to tell N from all of them: the container cap that dropped the rest, a pod list that never returned, a previous instance that produced nothing, and a read the outer backstop cut short all looked alike from outside. Every one of those lines now goes to capture-notes.txt as well, and the directory is created before the first of them, so a capture that found nothing is distinguishable from one that never ran. A pod list that failed is no longer reported as nothing having restarted. That is a claim about the cluster which the script never observed, and it is the claim most likely to talk a triager out of the crash-loop hypothesis on exactly the run where it is right. Exit 137 no longer asserts whose signal ended the read. 124 comes from timeout and from nothing else, but 137 is 128+SIGKILL, which the kill grace produces and so does the OOM killer or a teardown signalling the process group. Both belong in the cut-off branch, since the capture ends early either way, but reported flatly as the script's own timeout a read killed at second two reads as one that waited the full eighteen. COZY_PREVLOG_TAIL rejects a value kubectl cannot use and says so. A zero tail asks for no lines while kubectl still exits 0, which the empty-output branch would otherwise report as the container having logged nothing, discarding the only remaining copy of a log the kubelet has already collected. A leading zero is rejected because `$(( ))` reads it as octal while `[` reads it as decimal, so one string means two numbers. Each kept log names the bound it was written under, since --tail drops the oldest lines and nothing else in the output says so, and -1 suppresses that note because no bound was applied. An output directory that cannot be written is reported as the local condition it is. It arrives as an argument, so it can be a file, or missing, or unwritable; the redirection then fails before kubectl runs and hands the shell's own status to the branch that names kubectl, which reported "no previous instance retrieved (kubectl exit 2)" once per container. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
The failure snapshot selected pods by their STATUS column, so a pod whose container runs while its readiness probe never passes -- STATUS Running, READY 0/1 -- was dropped from the report entirely. That is a common way for a suite to time out: the replica never joins, its owning HelmRelease sits in InProgress, and the artifact carries no trace of the pod the run died on. Select on the READY column instead, which also covers the partially ready multi-container case, and keep every phase that was collected before. On one degraded cluster that is four of the seven broken pods the old filter reported nothing about. The VMI and VM sections had the same blind spot and now share one selector that reads READY as the last field, because a Pending object prints fewer columns than a Running one and the two kinds differ only in how many columns precede READY. READY counts ready containers rather than the pod's Ready condition, so a pod held back by an unsatisfied readiness gate is out of scope; a test pins that rather than leaving it to be found in an empty directory. Logs are requested for every container of a selected pod rather than skipped for Pending ones, and the per-pod collection moved into a helper so it can be driven against a stub kubectl. kubectl's refusal is the finding, and an empty result is ambiguous on its own: a read against an unscheduled pod exits 0 having printed nothing, which is what a container that started and stayed quiet also leaves. Every site that classifies a failed read now orders its branches the same way, naming the timeout first where both a cutoff and an uncapturable stderr apply. The CRD gate in the summary had no such branch at all: it returned in silence, which is byte-identical to the CRD being absent, so every section behind that gate vanished from a report whose only real problem was that the machine writing it had nowhere to put a temp file. Collection used to stop silently. A walk that hit its cap said so in the job log and nowhere in the artifact. Now the pod walk leaves COLLECTION-TRUNCATED.txt when it stops at COZY_REPORT_PODS_MAX or COZY_REPORT_PODS_BUDGET, CONTAINER-LOGS-TRUNCATED.txt on a pod cut off part way through, COLLECTION-FAILED.txt when the pod list never came back rather than the empty tree a healthy cluster produces, and COLLECTION-UNBOUNDED.txt when timeout is missing locally, which is a fact about the machine writing the report and not about the cluster. Tenant namespaces are collected first, so a cap that only bites on a broadly degraded cluster is not spent on platform pods while the pod the suite died on falls off the end. Container logs are read with --tail=2000 where they were previously read whole, and every bounded file states the bound it was written under, because --tail drops the oldest lines and nothing else in the output would say so. COZY_REPORT_POD_TAIL=-1 restores the complete log. The bounded whole-object read no longer merges stderr into pod.yaml, vm.yaml and vmi.yaml, which are read by yq rather than by eye: a warning on an otherwise complete read left an unparseable file. stderr goes to a scratch file and is copied in only when kubectl returned no object, and the truncation markers those files carry are written as YAML comments, since a bare marker line after a complete mapping is a second top-level node. Six latent faults surfaced while making these reads report. The kamaji module was gated on the LINSTOR controller, so a cluster without LINSTOR got no kamaji evidence at all, which is what a tenant-kubernetes bootstrap timeout needs most. The Flux sources section nested one jsonpath filter inside another, which kubectl rejects outright, so it printed nothing on every report ever taken and read as "all sources Ready". The HelmRelease listing took a single field as STATUS where STATUS is the Ready condition's whole message, so every failed release rendered as one word. The services section filtered for a pending LoadBalancer address on the CLUSTER-IP column, which never carries that value. The cozystack module read its image from a Deployment the installer stopped shipping when it moved to the operator, putting kubectl's NotFound in the first file of the report. And the cozystack apps section gated all three of its kinds on a CRD in an aggregated API group, so it never ran once; two of those names were wrong, and both are collected again. The tests avoid EXIT-trap cleanup, and e2e-testing.md says why: a bats test that installs one and then fails prints no TAP line at all, so output grepped for `not ok` reads as green, while hack/cozytest.sh reports the same failure correctly. A guard enforces that over a named list of files rather than a glob with exceptions, because a list of exceptions is the same claim written so that forgetting to shorten it makes it false. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
📝 WalkthroughWalkthroughThe PR adds bounded Kubernetes and container-log collection, preserves partial diagnostic output, expands report and capture tests, and replaces BATS ChangesDiagnostic collection
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant cozyreport.sh
participant kubectl
participant ReportArtifacts
cozyreport.sh->>kubectl: bounded resource and log reads
kubectl-->>cozyreport.sh: data, partial output, status, and stderr
cozyreport.sh->>ReportArtifacts: write reports and diagnostic notes
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
hack/capture-previous-logs.bats (1)
789-798: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the
-1suppression where the note is written.The run-level tail note goes to
capture-notes.txt, not into the.logfile. Line 794 therefore passes whether or not the[ "$TAIL" != "-1" ]guard athack/e2e-capture-previous-logs.shLine 429 exists.Assert on
capture-notes.txtso the test fails when the suppression is removed.♻️ Proposed change
- file="$(ls "$out"/*.log)" # -1 is the documented spelling for "the whole log". A note reading "holds at # most the last -1 lines" would describe a bound that was never applied, which # is the same class as claiming a truncated file is complete: a statement about # the artifact that the artifact does not support. - if grep -q 'holds at most' "$file"; then + if grep -q 'holds at most' "$out/capture-notes.txt"; then echo "FAIL: an unbounded read claims a tail bound" - cat "$file" + cat "$out/capture-notes.txt" false 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/capture-previous-logs.bats` around lines 789 - 798, Update the test around the `file` assignment to inspect `capture-notes.txt` instead of the captured `.log` file, and retain the existing `holds at most` assertion so it verifies the `TAIL=-1` suppression in `hack/e2e-capture-previous-logs.sh` rather than unrelated log content.hack/cozyreport.bats (1)
1907-1914: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive the kill grace from the script instead of hardcoding
5.The stub sets
COZYREPORT_BOUND="timeout -k 5 $COZYREPORT_READ_TIMEOUT". The timeout is read from the sourced script, but the grace is a literal.The assertion at Line 2380,
grep -c '^kill-after 5$', then compares against a value this file supplied. Ifhack/cozyreport.shchanges its grace, the stub keeps5and that assertion stays green while the stub no longer models production.Read the grace from the script the same way the timeout is read.
♻️ Proposed change
- COZYREPORT_BOUND="timeout -k 5 $COZYREPORT_READ_TIMEOUT" + # Grace read from the script, not written here: a literal makes the + # `kill-after` assertions compare against a value this file chose. + _grace=$(sed -n 's/.*timeout -k \([0-9]*\) .*/\1/p' "$SCRIPT" | head -n 1) + [ -n "$_grace" ] || { echo "FAIL: could not read the kill grace from $SCRIPT"; return 1; } + COZYREPORT_BOUND="timeout -k $_grace $COZYREPORT_READ_TIMEOUT"Then assert against
$_gracerather than the literal5at Line 2380.🤖 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/cozyreport.bats` around lines 1907 - 1914, Update the test stub setup around COZYREPORT_BOUND to read the timeout grace value from the sourced cozyreport.sh, alongside the existing read of COZYREPORT_READ_TIMEOUT, and construct the bound command with that variable instead of hardcoding 5. Update the related assertion to compare against $_grace so the stub and production script remain synchronized.hack/cozyreport-summary.sh (1)
257-257: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueOptional: add
|| truefor parity with the collector's call.This script runs under
set -e.cozyreport_pods_prioritizereturns 0 on both of its paths today, so the pipeline cannot abort the script now. The collector guards the identical pipeline with|| trueathack/cozyreport.shline 1107. Matching that here removes the dependency on the helper's exit status.♻️ Proposed change
-POD_ROWS=$(printf '%s\n' "$POD_LIST" | cozyreport_pods_not_ready | cozyreport_pods_prioritize) +POD_ROWS=$(printf '%s\n' "$POD_LIST" | cozyreport_pods_not_ready | cozyreport_pods_prioritize) || true🤖 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/cozyreport-summary.sh` at line 257, Add `|| true` to the `POD_ROWS` pipeline involving `cozyreport_pods_not_ready` and `cozyreport_pods_prioritize`, matching the guarded collector pipeline so `set -e` does not abort the summary script if the helper’s exit status changes.hack/cozyreport.sh (2)
1424-1440: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: add
-rto the three object loops, and clearCOZYREPORT_OBJECTS_DEADLINEafter the last walk.ShellCheck flags lines 1431, 1451 and 1471 for
readwithout-r(SC2162). Namespace and object names cannot contain backslashes, so the current form is safe today.-rremoves the warning and matches theread -rused incozyreport_collect_broken_pods.
COZYREPORT_OBJECTS_DEADLINEstays set after the services walk. Each walk overwrites it before use, so there is no current defect.cozyreport_collect_broken_podsclearsCOZYREPORT_COLLECT_DEADLINEat line 1205 for the stated reason that a stale deadline silently bounds a later reader. The same reason applies here.♻️ Proposed changes
cozyreport_select_objects "virtualmachines" kubectl get vm -A --no-headers | cozyreport_kubevirt_not_ready | cozyreport_admit_objects "virtualmachines" | - while read NAMESPACE NAME _; do + while read -r NAMESPACE NAME _; doApply the same change at lines 1451 and 1471, and clear the deadline after the services walk:
COZYREPORT_OBJECTS_DEADLINE=""🤖 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/cozyreport.sh` around lines 1424 - 1440, Update all three object-collection loops to use read -r, including the loop around cozyreport_select_objects and the corresponding loops near the other object walks. After the final services walk, clear COZYREPORT_OBJECTS_DEADLINE by assigning it an empty value, matching the cleanup pattern used by cozyreport_collect_broken_pods.Source: Linters/SAST tools
1264-1271: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueQuote the report output file paths.
REPORT_DIRderives frommktemp -d, so these paths should not be affected by filenames with spaces. ShellCheck still reports SC2086, so quote themkdirandkubectlredirect paths for consistency.♻️ Quoting fix
-mkdir -p $REPORT_DIR/cozystack +mkdir -p "$REPORT_DIR/cozystack"-kubectl get deploy -n cozy-system cozystack-operator -o jsonpath='{.spec.template.spec.containers[0].image}' > $REPORT_DIR/cozystack/image.txt 2>&1 +kubectl get deploy -n cozy-system cozystack-operator -o jsonpath='{.spec.template.spec.containers[0].image}' > "$REPORT_DIR/cozystack/image.txt" 2>&1🤖 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/cozyreport.sh` around lines 1264 - 1271, Quote the REPORT_DIR-derived paths in the mkdir command and the kubectl output redirection within the cozystack report block to resolve SC2086, preserving the existing directory and report filenames.Source: Linters/SAST tools
🤖 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/capture-previous-logs.bats`:
- Around line 365-374: Fix the pairing assertion around the `pairs` validation
loop so failures propagate to the Bats test instead of being confined to a
pipeline subshell. Replace the piped `printf ... | while` structure with
process-substitution or capture offending pairs and assert afterward, while
preserving the existing accepted `$PREVLOG_READ_TIMEOUT/$PREVLOG_BOUND` and
`$PREVLOG_LIST_TIMEOUT/$PREVLOG_LIST_BOUND` mappings.
- Around line 351-354: Update the extraction guard in the test setup to capture
and validate each of the four sed-derived values independently: the list bound,
list timeout, read grace, and read timeout, along with the existing cap value.
Do not concatenate paired values before checking non-emptiness; ensure the
arithmetic checks at the following lines are only reached when every extracted
value is present.
---
Nitpick comments:
In `@hack/capture-previous-logs.bats`:
- Around line 789-798: Update the test around the `file` assignment to inspect
`capture-notes.txt` instead of the captured `.log` file, and retain the existing
`holds at most` assertion so it verifies the `TAIL=-1` suppression in
`hack/e2e-capture-previous-logs.sh` rather than unrelated log content.
In `@hack/cozyreport-summary.sh`:
- Line 257: Add `|| true` to the `POD_ROWS` pipeline involving
`cozyreport_pods_not_ready` and `cozyreport_pods_prioritize`, matching the
guarded collector pipeline so `set -e` does not abort the summary script if the
helper’s exit status changes.
In `@hack/cozyreport.bats`:
- Around line 1907-1914: Update the test stub setup around COZYREPORT_BOUND to
read the timeout grace value from the sourced cozyreport.sh, alongside the
existing read of COZYREPORT_READ_TIMEOUT, and construct the bound command with
that variable instead of hardcoding 5. Update the related assertion to compare
against $_grace so the stub and production script remain synchronized.
In `@hack/cozyreport.sh`:
- Around line 1424-1440: Update all three object-collection loops to use read
-r, including the loop around cozyreport_select_objects and the corresponding
loops near the other object walks. After the final services walk, clear
COZYREPORT_OBJECTS_DEADLINE by assigning it an empty value, matching the cleanup
pattern used by cozyreport_collect_broken_pods.
- Around line 1264-1271: Quote the REPORT_DIR-derived paths in the mkdir command
and the kubectl output redirection within the cozystack report block to resolve
SC2086, preserving the existing directory and report filenames.
🪄 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: 9e1179f0-b399-4338-a513-8d427c7513ec
📒 Files selected for processing (15)
docs/agents/e2e-testing.mdhack/admin-kubeconfig-invariant.batshack/capture-previous-logs.batshack/check-gpu-operator-variants.batshack/check-gpu-recording-rules.batshack/check-host-runtime.batshack/cozyreport-summary.shhack/cozyreport-talos.batshack/cozyreport.batshack/cozyreport.shhack/e2e-capture-previous-logs.shhack/e2e-chainsaw/.chainsaw.yamlhack/promote-retag_test.batshack/remediation-guard.batshack/select-install_test.bats
| list="$(sed -n 's/^ PREVLOG_LIST_BOUND="timeout -k \([0-9]*\) .*"/\1/p' "$SCRIPT") $(sed -n 's/^PREVLOG_LIST_TIMEOUT=\([0-9]*\)/\1/p' "$SCRIPT")" | ||
| read_="$(sed -n 's/^PREVLOG_READ_GRACE=\([0-9]*\)/\1/p' "$SCRIPT") $(sed -n 's/^PREVLOG_READ_TIMEOUT=\([0-9]*\)/\1/p' "$SCRIPT")" | ||
| cap=$(sed -n 's/^MAX="${COZY_PREVLOG_MAX:-\([0-9]*\)}"/\1/p' "$SCRIPT") | ||
| [ -n "$list" ] && [ -n "$read_" ] && [ -n "$cap" ] |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
The emptiness guard cannot fail.
list and read_ are each built as "$a $b", so both always contain the joining space. [ -n "$list" ] and [ -n "$read_" ] therefore always succeed, even when a sed pattern matches nothing.
If one pattern stops matching, cut -d' ' -f1 yields an empty string and the arithmetic at Line 356 or Line 357 becomes $(( + 28 )). Under dash that aborts the test body instead of reporting the intended failure, so the guard reports nothing at the moment it is needed.
Check the four extracted values individually.
🛠️ Proposed fix
- list="$(sed -n 's/^ PREVLOG_LIST_BOUND="timeout -k \([0-9]*\) .*"/\1/p' "$SCRIPT") $(sed -n 's/^PREVLOG_LIST_TIMEOUT=\([0-9]*\)/\1/p' "$SCRIPT")"
- read_="$(sed -n 's/^PREVLOG_READ_GRACE=\([0-9]*\)/\1/p' "$SCRIPT") $(sed -n 's/^PREVLOG_READ_TIMEOUT=\([0-9]*\)/\1/p' "$SCRIPT")"
+ list_grace=$(sed -n 's/^ PREVLOG_LIST_BOUND="timeout -k \([0-9]*\) .*"/\1/p' "$SCRIPT")
+ list_secs=$(sed -n 's/^PREVLOG_LIST_TIMEOUT=\([0-9]*\)/\1/p' "$SCRIPT")
+ read_grace=$(sed -n 's/^PREVLOG_READ_GRACE=\([0-9]*\)/\1/p' "$SCRIPT")
+ read_secs=$(sed -n 's/^PREVLOG_READ_TIMEOUT=\([0-9]*\)/\1/p' "$SCRIPT")
cap=$(sed -n 's/^MAX="${COZY_PREVLOG_MAX:-\([0-9]*\)}"/\1/p' "$SCRIPT")
- [ -n "$list" ] && [ -n "$read_" ] && [ -n "$cap" ]
+ for v in "$list_grace" "$list_secs" "$read_grace" "$read_secs" "$cap"; do
+ [ -n "$v" ] || { echo "FAIL: a bound could no longer be read from $SCRIPT"; false; }
+ done
- list_ceiling=$(( $(echo "$list" | cut -d' ' -f1) + $(echo "$list" | cut -d' ' -f2) ))
- read_ceiling=$(( $(echo "$read_" | cut -d' ' -f1) + $(echo "$read_" | cut -d' ' -f2) ))
+ list_ceiling=$(( list_grace + list_secs ))
+ read_ceiling=$(( read_grace + read_secs ))
worst=$(( list_ceiling + cap * read_ceiling ))🤖 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/capture-previous-logs.bats` around lines 351 - 354, Update the
extraction guard in the test setup to capture and validate each of the four
sed-derived values independently: the list bound, list timeout, read grace, and
read timeout, along with the existing cap value. Do not concatenate paired
values before checking non-emptiness; ensure the arithmetic checks at the
following lines are only reached when every extracted value is present.
| pairs=$(grep -o 'prevlog_cutoff_desc "[^"]*" "\$[A-Z_]*" "\$[A-Z_]*"' "$SCRIPT" | | ||
| sed 's/.*"\(\$[A-Z_]*\)" "\(\$[A-Z_]*\)"/\1 \2/' | LC_ALL=C sort -u) | ||
| [ -n "$pairs" ] || { echo "FAIL: no cut-off description is derived from a bound"; false; } | ||
| printf '%s\n' "$pairs" | while read -r secs bound; do | ||
| case "$secs $bound" in | ||
| '$PREVLOG_READ_TIMEOUT $PREVLOG_BOUND') ;; | ||
| '$PREVLOG_LIST_TIMEOUT $PREVLOG_LIST_BOUND') ;; | ||
| *) echo "FAIL: $secs is quoted for a read bounded by $bound"; exit 1 ;; | ||
| esac | ||
| done |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
The pairing check cannot fail the test.
The while loop reads from a pipe, so it runs in a subshell. exit 1 at Line 372 ends only that subshell. The pipeline's status is never tested, so the test continues to the backstop loop and passes.
A prevlog_cutoff_desc call that quotes the wrong seconds variable therefore stays green, which is the failure mode this test exists to prevent.
Feed the loop without a pipe, or capture the offending pairs and assert on them.
🛠️ Proposed fix
- printf '%s\n' "$pairs" | while read -r secs bound; do
- case "$secs $bound" in
- '$PREVLOG_READ_TIMEOUT $PREVLOG_BOUND') ;;
- '$PREVLOG_LIST_TIMEOUT $PREVLOG_LIST_BOUND') ;;
- *) echo "FAIL: $secs is quoted for a read bounded by $bound"; exit 1 ;;
- esac
- done
+ bad_pairs=""
+ while read -r secs bound; do
+ [ -n "$secs" ] || continue
+ case "$secs $bound" in
+ '$PREVLOG_READ_TIMEOUT $PREVLOG_BOUND') ;;
+ '$PREVLOG_LIST_TIMEOUT $PREVLOG_LIST_BOUND') ;;
+ *) bad_pairs="$bad_pairs
+$secs is quoted for a read bounded by $bound" ;;
+ esac
+ done <<EOF
+$pairs
+EOF
+ if [ -n "$bad_pairs" ]; then
+ echo "FAIL: a cut-off description quotes a budget its read never used:$bad_pairs"
+ false
+ fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pairs=$(grep -o 'prevlog_cutoff_desc "[^"]*" "\$[A-Z_]*" "\$[A-Z_]*"' "$SCRIPT" | | |
| sed 's/.*"\(\$[A-Z_]*\)" "\(\$[A-Z_]*\)"/\1 \2/' | LC_ALL=C sort -u) | |
| [ -n "$pairs" ] || { echo "FAIL: no cut-off description is derived from a bound"; false; } | |
| printf '%s\n' "$pairs" | while read -r secs bound; do | |
| case "$secs $bound" in | |
| '$PREVLOG_READ_TIMEOUT $PREVLOG_BOUND') ;; | |
| '$PREVLOG_LIST_TIMEOUT $PREVLOG_LIST_BOUND') ;; | |
| *) echo "FAIL: $secs is quoted for a read bounded by $bound"; exit 1 ;; | |
| esac | |
| done | |
| pairs=$(grep -o 'prevlog_cutoff_desc "[^"]*" "\$[A-Z_]*" "\$[A-Z_]*"' "$SCRIPT" | | |
| sed 's/.*"\(\$[A-Z_]*\)" "\(\$[A-Z_]*\)"/\1 \2/' | LC_ALL=C sort -u) | |
| [ -n "$pairs" ] || { echo "FAIL: no cut-off description is derived from a bound"; false; } | |
| bad_pairs="" | |
| while read -r secs bound; do | |
| [ -n "$secs" ] || continue | |
| case "$secs $bound" in | |
| '$PREVLOG_READ_TIMEOUT $PREVLOG_BOUND') ;; | |
| '$PREVLOG_LIST_TIMEOUT $PREVLOG_LIST_BOUND') ;; | |
| *) bad_pairs="$bad_pairs | |
| $secs is quoted for a read bounded by $bound" ;; | |
| esac | |
| done <<EOF | |
| $pairs | |
| EOF | |
| if [ -n "$bad_pairs" ]; then | |
| echo "FAIL: a cut-off description quotes a budget its read never used:$bad_pairs" | |
| false | |
| 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/capture-previous-logs.bats` around lines 365 - 374, Fix the pairing
assertion around the `pairs` validation loop so failures propagate to the Bats
test instead of being confined to a pipeline subshell. Replace the piped `printf
... | while` structure with process-substitution or capture offending pairs and
assert afterward, while preserving the existing accepted
`$PREVLOG_READ_TIMEOUT/$PREVLOG_BOUND` and
`$PREVLOG_LIST_TIMEOUT/$PREVLOG_LIST_BOUND` mappings.
|
The red is outside this diff. Run Timings come from the tenant crust-gather this run collected. The collectors this PR changes did run, on live data.
|
IvanHunters
left a comment
There was a problem hiding this comment.
LGTM.
Diagnostics-tooling bugfix only (hack/*; nothing under packages/, api/ or charts). The +7169 is mostly tests (cozyreport.bats +4356, capture-previous-logs.bats +711); real logic is ~2400 lines, no committed binary fixtures. The three column-selector fixes and two gate fixes are verified against real kubectl column layouts and confirmed by behavioural tests; error handling is fail-closed, not fail-open. Regression-free on supported paths.
Non-blocking notes:
- Some module gates (cozystack-operator-log, cert-manager, objectstorage/COSI, linstor) still collapse NotFound and can't-ask into one — a transient apiserver failure reads as "component absent", the exact class this PR fixes elsewhere. No live bug (NotFound dominates and summary_crd partially compensates), but inconsistent; consider routing them through the same discriminating reader.
- Header budget arithmetic is understated by ~100-150s (omits the per-pod container-list read and the per-section selector reads). Low impact since the header itself calls the number a FLOOR.
- cozyreport.sh object-helpers write to global $REPORT_DIR instead of an argument, unlike the sibling helper. No live bug today, but a future caller without REPORT_DIR would write from filesystem root and
|| truewould swallow the failure — the "never truncate silently" mechanism silencing itself. Consider parameterizing as in _cbp_root. - The capture-previous-logs loop has no internal wall-clock budget and relies on an external timeout in another file; raising COZY_PREVLOG_MAX past ~13 risks SIGKILL mid-loop with no "cut short" marker. Trade-off is documented in the docstring.
The ban on EXIT-trap cleanup in hack/*.bats was enforced by two lists inside hack/cozyreport.bats: one naming the files known to be clean, and one freezing an exact trap count for each file that was not. Both had to be edited from whatever change moved them, and that is where the guard failed, and it never worked even once. The inventory landed in #3567 at 15:25:20; #3195 had landed multus-install-cni-plugins.bats carrying twelve traps forty-three seconds earlier, so replaying the old guard against the tree at the commit that introduced it already gives found != frozen. Main stayed red for about twenty-two and three quarter hours. The first repair, #3584, landed already red the next morning because #3548 had brought in run-kubernetes-talos-diagnostics_test.bats with eight traps two minutes ahead of it, so that fix bought no green time at all; green came only with the second repair. Neither pair of PRs shared a line, and each was green against its own base. A change adding a trap to its own file had to edit a string in a suite it otherwise never touches, so two changes sharing no line still invalidated each other: each stayed green against its own base, git merged both cleanly, and the guard went red only once the second landed. A file arriving with traps hit the same wall from the other side -- the inventory did notice it, since the string it compared was built by scanning the directory, but absorbing it meant an edit in a file nothing in the author's diff pointed at. Replace both lists with a declaration each file makes about itself: one "# EXIT-TRAP DEBT: N" comment, exact rather than a ceiling. A file carrying none must install no EXIT trap, which covers the converted files, the files that never had one, and every file added later. Growing or shedding a trap now fails in the file the change already edits, so two changes that disagree about a count collide textually instead of silently. That collision is a steady-state property, and this change's own arrival is the exception. A branch forked before the declaration existed has no line to disagree with: it converts traps in its own file, merges clean, and the count goes wrong only once both sides are on main -- verified against a sibling branch that takes select-e2e_test.bats from fifteen traps to zero. What the move buys even then is that the red names a file that branch already edited, the repair is one line inside it, and rebasing before merge catches it on the branch's own CI. None of those three held against the inventory. The declaration is read from the leading comment block, not from anywhere in the file. A .bats file is shell that writes shell, so the same line turns up inside a heredoc, a fixture writer or an expected-output string, where it is data belonging to one test. Reading it there as a statement about the whole file would let an unrelated fixture excuse a real trap, and would do it silently, since nothing in that test's own diff looks like a declaration. A comment block is the region with no interior: stopping instead at the first @test would still read a line out of a helper's heredoc. Not every counted handler is debt. A trap inside an explicit subshell does not replace the bats binary's own, so a test failing inside `( ... )` still prints its `not ok`. hack/e2e-test-openapi.bats kills a backgrounded kubectl proxy that way, and moving the kill to the end of the body would leak a process holding a fixed port. Its declaration records that rather than scheduling a conversion, and because the ratchet is exact, removing the trap fails too -- the count protects the construct. What the count cannot do is tell the two apart: substituting a test-level trap for the subshell one keeps the total at 1 and stays green, which the header states rather than leaves to be found. Counting bounds the keyword and the signal the same way, at any character that cannot be part of an identifier, and matches the signal in either case. Whitespace on the right missed `trap ... EXIT; cd "$tmp"`; whitespace on the left missed `tmp=$(mktemp -d);trap ... EXIT` and `(trap ... EXIT; true)`; upper case missed `trap ... exit`, which bash and dash both install. All are real handlers that scored zero, and the left boundary matters most, since the inventory being replaced had none and caught the semicolon form. A bare word boundary is not enough either way: `bootstrap ` ends in `trap `, and it must keep scoring nothing. A quoted signal counts for the same reason as the rest. Two handlers sharing one line are reported rather than counted, since the count is a count of lines and the second would otherwise arrive without moving the total; splitting the line properly needs a shell parser, a semicolon inside a handler's own action not being a separator, and guessing wrong undercounts -- the one direction a ratchet cannot afford. The scan recurses, so hack/e2e-apps/*.bats is covered rather than sitting one directory below the guard that claims the tree. It reads .bats and nothing else, so a handler arriving through a sourced .sh stays outside it -- hack/e2e-chainsaw/_lib/run-kubernetes.sh installs two, each benign for its own reason rather than by design: one sits in a function declared with `(` and so runs in a subshell, the other in a brace function no @test calls. That boundary is stated in the header rather than papered over. The guard moves to hack/bats-no-exit-trap.bats: its subject is every unit suite under hack/, not the report collector it grew up in. Its fixture helpers assemble the trap keyword and the signal from separate arguments, because the guard scans its own source and a fixture written as a literal would be counted as a real trap in it. Assisted-By: Claude <[email protected]> Signed-off-by: Aleksei Sviridkin <[email protected]>
…#3622) <!-- Thank you for making a contribution! Here are some tips for you: - Use Conventional Commits for the PR title: `type(scope): description` - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore - Scopes are not an exhaustive list — pick the most specific scope for the change and extend the list when a genuinely new area appears. Examples: - System components: dashboard, platform, operator, cilium, kube-ovn, linstor, fluxcd, cluster-api - Managed apps: postgres, mariadb, redis, kafka, clickhouse, virtual-machine, kubernetes - Development and maintenance: api, hack, tests, ci, docs, maintenance - Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or add a `BREAKING CHANGE:` footer - If it's a work in progress, consider creating this PR as a draft. - Don't hesistate to ask for opinion and review in the community chats, even if it's still a draft. - Add the label `backport` if it's a bugfix that needs to be backported to a previous version. --> ## What this PR does The ban on EXIT-trap cleanup in `hack/*.bats` was enforced by two lists inside `hack/cozyreport.bats`: one naming the files known to be clean, one freezing an exact trap count for each file that was not. Both had to be edited from whatever change moved them, and that is where the guard kept failing. It is worth being exact about how badly, because the history is sharper than "it went stale a few times". **The inventory was never correct on main for a single commit.** It landed in #3567 at 15:25:20. #3195 had landed `multus-install-cni-plugins.bats` carrying twelve traps at 15:24:37, forty-three seconds earlier. Replaying the old guard's own logic against the tree at the very commit that introduced it already gives `found != frozen`. Main then stayed red for roughly twenty-two and three quarter hours. The first repair, #3584, landed *already red* the next morning at 10:42:43, because #3548 had brought in `run-kubernetes-talos-diagnostics_test.bats` with eight traps at 10:40:03, under three minutes ahead of it. That fix was correct and bought zero green time. Green arrived only with the second repair, #3602. Neither pair of PRs shared a line, and each was green against its own base. That is the whole mechanism, and it is why a third one-line repair is not the answer. The mechanism is structural rather than careless. A change that adds a trap to its own file had to edit a string in a suite it otherwise never touches, so two changes sharing no line still invalidated each other: each stayed green against its own base, git merged both cleanly, and the guard went red only once the second one landed. A file *arriving* with traps was worse still, because nothing in its author's diff pointed at that string at all. That is exactly how `run-kubernetes-talos-diagnostics_test.bats` got in, twice. That contention is not in the past tense. Two open PRs are editing that one line right now, and they disagree about what it should say: #3575 adds `run-kubernetes-talos-diagnostics_test.bats=8` to it, repeating a repair that has already landed, and #3441 removes `select-e2e_test.bats=15` from it, because it converts that file. Neither PR is about EXIT traps. Both have to touch that string anyway, and whichever lands second is wrong until someone edits it again. So this PR is not fixing a red main. It removes the thing that keeps making main red, which is why it is worth more than the one-line fix that is now the established habit. One honest caveat about its own landing. The textual-conflict property is steady-state: a branch forked *before* the declaration exists has no line to disagree with, so it converts traps in its own file, merges clean, and the count only goes wrong once both sides are on main. I checked this against #3441, which takes `select-e2e_test.bats` from fifteen traps to zero. Merged after this, that file would declare fifteen and hold none. What the move buys even in that case is that the red names a file the branch already edited, the repair is one line inside it, and rebasing before merge catches it on the branch's own CI. None of those three held against the central inventory. Whoever merges this should expect one such adjustment on the conversion branches still in flight. So this replaces both lists with a declaration each file makes about itself: one `# EXIT-TRAP DEBT: N` comment in its leading comment block. A file carrying no declaration must install no EXIT trap. Growing or shedding a trap now fails in the file the change already edits, so two changes that disagree about a count get a real textual conflict instead of silently invalidating each other, and a change that leaves the traps alone edits nothing. **The include list is redundant, not lost.** It named the files proven clean, so that a trap reappearing in one of them would fail. Under the new rule those files carry no declaration, and a file with no declaration must hold zero traps, so a trap reappearing in any of them fails on its own, with no list to be on. Coverage widens rather than narrows: the two lists named twenty files between them, and the rule covers all fifty bats files under `hack/`, subdirectories included, plus the ones added tomorrow. Rebasing this branch onto current main is the property working. Main has since gained `hack/kubernetes-pre-delete-hook.bats` and `hack/tenant-pre-delete-hook.bats`, and `hack/cozyreport.bats` grew by some eight hundred lines. Neither new file installs an EXIT trap, so neither needed a declaration and neither needed an edit here; the rebase took no conflict at all. Under the inventory, each arriving file was a coin toss on whether somebody had remembered the string. To be precise about what the inventory could and could not do, since it is easy to overstate: it did *notice* a new file carrying traps. The string it compared was built by scanning the directory, so an arriving file appended a token and failed the comparison, which is exactly how main went red. What it could not do is let that file arrive without an edit in a foreign suite. Being seen and being absorbable are different properties, and only the second one decides whether two changes can land independently. The declaration is pinned in both directions. Declaring N while holding N+1 fails, obviously; declaring N while holding N−1 fails too. Without that second half the number becomes a ceiling and rots upward: somebody converts half a file, the declaration stays, and the guard quietly licenses traps that were removed long ago. It is read only from the leading comment block, under the shebang and above the first line of code. A `.bats` file is shell that writes shell, so the same line turns up inside a heredoc, a fixture writer or an expected-output string, where it is data belonging to one test; honouring it there would let an unrelated fixture excuse a real trap, silently, with nothing in that test's own diff looking like a declaration. A comment block is the region with no interior; stopping instead at the first `@test` would still read a line out of a helper's heredoc. Not every counted handler is debt. A trap inside an explicit subshell does not replace the one the `bats` binary installs, so a test failing inside `( … )` still prints its `not ok`, checked against a test-level trap in the same file, where the TAP line vanishes. `hack/e2e-test-openapi.bats` kills a backgrounded `kubectl proxy` exactly that way, and "convert it like the others" would leak a process holding a fixed port and wedge the next run. Its declaration now records the carve-out instead of scheduling a conversion, and because the ratchet is exact in both directions, *removing* that trap fails too, so the count protects the construct rather than marking it for deletion. `docs/agents/e2e-testing.md` previously scoped this exception to Chainsaw `script` steps only; it now names the BATS subshell case as well. The counting bounds the keyword and the signal the same way, at any character that cannot be part of an identifier, and matches the signal in either case. Whitespace on the right missed `trap … EXIT; cd "$tmp"`; whitespace on the left missed `tmp=$(mktemp -d);trap … EXIT` and `(trap … EXIT; true)`; upper case missed `trap … exit`, which bash and dash both install. All of those are real handlers that scored zero. The left boundary is the one worth dwelling on, because the inventory being replaced had none at all and *did* catch the semicolon form. Getting it wrong here would have narrowed coverage while the commit claimed to widen it. A plain word boundary is not enough either: `bootstrap ` ends in `trap `, and it has to keep scoring nothing, or the documented answer to a red guard (add a debt line) would buy a file a permanent licence for one real trap to silence a line that has none. Two handlers sharing one line are reported rather than counted, because the count is a count of lines and the second would otherwise arrive free. Splitting such a line properly needs a shell parser, since a semicolon inside a handler's own quoted action is not a separator, and guessing wrong undercounts, the one direction a ratchet cannot afford. **What this does not fix.** The declaration is still a loophole: a new file can write `# EXIT-TRAP DEBT: 8` instead of cleaning up, and nothing here makes that impossible. What changes is that the admission is local and visible. It sits at the top of the file it excuses, in front of whoever reviews that file, instead of being a number in a neighbouring suite nobody in that review is reading. Today's loophole is the same size and invisible. Three more limits, all stated in the guard's own header rather than left to be discovered. The scan is lexical, so a signal computed at runtime and a quoted action spanning physical lines without a backslash are both invisible. An exact count catches addition and removal but never substitution: swap the openapi file's subshell trap for a test-level one and the total stays 1. And the scan reads `.bats` only, so a handler arriving through a sourced `.sh` is outside it. `hack/e2e-chainsaw/_lib/run-kubernetes.sh` installs two right now, and each is benign for its own reason rather than by design: the one in `cozy_capture_tenant_talos` because that function is declared with `(` and so runs in a subshell, the one in `run_kubernetes_test` because no `@test` calls it despite being declared with `{`. Three `hack/*.bats` source that library, and two tests in the converted file call `cozy_capture_tenant_talos`, so flipping a single `(` to `{` reinstates a test-level handler in both of them with the guard green. Widening the scan to `.sh` would mean counting handlers that are correct in a script and wrong only in a test body, so the honest answer is that this is where the instrument stops. Three further boundaries, recorded here so they land as known edges rather than as surprises. The old include list also failed when a file named on it disappeared from the tree; the new rule can only judge a file that is present, so a deleted converted file goes unnoticed. That is a genuinely smaller check, though its absence shows up in the diff that deletes the file. The guard's own failure messages are code lines, so they are scanned by the pattern they belong to: they pass today only because no bare `EXIT` or `0` happens to follow the keyword in any of them, and a rewording that introduced one would make the file demand a debt of itself. It fails loudly rather than quietly, and the fixture writers and test titles already split the keyword from the signal for this reason, but the messages do not. Finally, `docs/agents/e2e-testing.md` bans test-level `EXIT` *and* `RETURN` traps, while every mechanical guard this repo has had, the one being deleted included, matches only `EXIT` and `0`. `hack/` holds no RETURN trap today, so nothing regresses here, but half of that documented rule has never had an executor. The guard moves out of `hack/cozyreport.bats` into `hack/bats-no-exit-trap.bats`, because its subject is every unit suite under `hack/` and not the report collector it grew up in. Living inside `cozyreport.bats` is precisely why unrelated PRs kept converging on one line. `hack/md-no-hardwrap.bats` is the neighbouring precedent. Separately, the eight EXIT traps in `hack/run-kubernetes-talos-diagnostics_test.bats` are converted to end-of-body cleanup rather than given a declaration. That file was written six weeks after the ban was documented, so "it predates the rule" is not available to it, and handing it a debt is literally the move that failed the last two times. The remaining files keep their declarations, since a mass conversion would collide with the branches that own them. One note on where the prose sits. The guard's own header carries only what a reader has to keep true: why the ban exists, the two carve-outs, where the declaration lives and why there, that the count is exact in both directions, why the marker is anchored at column zero, and where the lexical scan stops. Everything above about *how* the inventory failed (the timestamps, the PR numbers, the arithmetic) is deliberately not in the file. It is true today and will rot on its own; a comment nobody is obliged to maintain is how a file grows a paragraph that is quietly false. ### Screenshots <!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating the visual impact of your changes. PRs with UI changes without screenshots will not be merged. --> ### Downstream repositories <!-- Cozystack is upstream for repositories that are NOT kept in sync with it automatically, and nothing in CI compares the two sides, so a change here can break them silently. Which change forces what, and which file to touch, is the trigger map in docs/agents/contributing.md under "Downstream Repositories". Walk it before you tick anything. IF YOU ARE AN AI AGENT OR AN LLM, THIS IS PART OF THE TASK, NOT A FORM TO FILL IN: 1. Walk the trigger map against the actual diff of this PR, file by file. Do not decide from the PR title or from your memory of what you changed. 2. For every repository you tick, open the follow-up PR there and link it on the same line. A ticked box with no link claims work that does not exist, which is worse than an empty box. Search that repository for an open PR or issue covering it first, and link that instead of filing a duplicate. 3. When the follow-up is out of scope, or needs a decision that is not yours to make, open an issue in that repository instead, link that, and say so in the PR body. Do not push a speculative PR into another repository just to fill a line here. 4. Do not tick "No downstream repository is affected" to make the checklist go away. If you are unsure, leave every box empty and say so in the PR body, so a human decides. --> - [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: ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same `type(scope):` prefix as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note test(tests): each hack/*.bats file now declares its own remaining EXIT-trap debt in a `# EXIT-TRAP DEBT: N` header comment, checked by hack/bats-no-exit-trap.bats, replacing the central inventory in hack/cozyreport.bats ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Testing** - Added automated auditing for `EXIT` traps across Bats end-to-end tests, including validation of tracking declarations and edge cases. - Improved diagnostics tests by replacing trap-based temporary-directory cleanup with explicit cleanup steps. - Added tracking annotations for remaining trap-related cleanup work. - **Documentation** - Clarified when traps are permitted inside self-contained subshells and how remaining cleanup debt is reported. - Updated review guidance for consistent end-to-end test maintenance. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What this PR does
The e2e diagnostic tarball exists so a red run can be explained from the uploaded artifact alone. Several of its collectors answered "nothing is wrong" instead — some because they selected on a column that never carries the value they filtered for, others because they stopped early and left nothing saying they had. Both failure modes look identical to a healthy cluster from the outside, which is the worst possible shape for a diagnostic.
Four selectors in
hack/cozyreport.shandhack/cozyreport-summary.shwere reading the wrong thing:0/1 Runningby a readiness probe that never passes was skipped. That is a common way for a suite to time out, and the artifact carried no trace of the pod the run died on. Selection now compares the two halves of the READY column; terminalSucceeded/Completedpods stay excluded.$4, which is CLUSTER-IP. CLUSTER-IP holds an address,Noneor<none>and never<pending>, so that loop had never executed.<pending>only ever appears in EXTERNAL-IP,$5.kubectl get crd <kind>.apps.cozystack.io. ApplicationDefinition is a CRD but in a different group, and Tenant comes from an aggregated APIService with no CRD to ask about, so the probe failed and the section never ran.cozy-linstor'slinstor-controllerdeployment rather than kamaji's own, so a cluster without LINSTOR got no kamaji logs and no TenantControlPlanes — on exactly the runs that need them.The summary had three of its own: the HelmRelease listing printed
$5where STATUS is the Ready condition's whole message, reducing every failed release to one word like "Helm"; the Flux sources listing used a jsonpath with a nested filter that kubectl rejects outright, so it printed nothing on every report ever taken; and pod AGE was read as$6, which shifts whenever RESTARTS prints the7 (4m12s ago)form — that is, for exactly the rows worth reading.The second half is bounding. The pod, VM, VMI and services walks now run under a count cap and a wall-clock budget, and every read is individually time-boxed. The point is not the bounds themselves but that each one names itself when it fires: a walk stopped by the cap or the clock leaves
COLLECTION-TRUNCATED.txt, a pod list that never returned leavesCOLLECTION-FAILED.txtinstead of the empty tree a healthy cluster produces, a log cut short says so on its own last line, and a--tailbound is recorded beside the logs rather than inside them, since the platform's controllers log JSON per line and a trailing prose line costs a parser the whole file. A knob value the collector could not use is reported too, so a truncation note quoting600is never mistaken for the10msomebody actually set. Wheretimeoutis absent the reads run unbounded rather than not at all, and the section says which of the two happened, because a missing local binary is not a statement about the cluster.One behaviour change worth stating plainly, because it is a trade and not a pure gain: the per-pod section used to be uncapped and read container logs whole, and it is now bounded by
COZY_REPORT_PODS_MAX(40),COZY_REPORT_PODS_BUDGET(600s) andCOZY_REPORT_POD_TAIL(2000). Below those thresholds the new tree is a strict superset of the old one — the old selector missedRunning 0/1entirely and skipped logs forPendingpods. Above them the artifact is smaller than before, every drop is named in the tree, andCOZY_REPORT_POD_TAIL=-1with a largerCOZY_REPORT_PODS_MAXrestores the old reach. Because widening the selection and capping the walk in one change could otherwise let a newly-in-scope pod evict one the uncapped walk always collected, the ordering splits by evidence class before namespace: pods that are notRunning— the class the collector took before this — all rank ahead of pods that areRunningbut unready, so the cap can only ever drop a pod the old collector did not keep either.hack/e2e-capture-previous-logs.shgets the same treatment: its notes now go tocapture-notes.txtin the artifact as well as to the job log, and a pod list that never returned is reported as such rather than as "nothing restarted". That script now creates its own output directory before its first line of output, which makes the comment inhack/e2e-chainsaw/.chainsaw.yamlwrong about why the once-per-test guard there holds — the only change to that file is the corrected comment, saying the-dcheck keys on whether the catch block already ran rather than on what the helper found.The rest is test coverage — a new
hack/cozyreport.bats, an extendedhack/capture-previous-logs.bats— and the removal ofEXIT-trap cleanup from the bats files those tests now cover. That trap is worth its own note: inside an@testbody it replaces the one thebatsbinary installs for its own bookkeeping, so a test that then fails prints no TAP line at all. It does not appear asnot ok; it disappears, and only the# bats warning: Executed N instead of expected M testsline at the end distinguishes the run from a green one. A guard test enforces the ban over a named list of files that grows as the remaining ones are converted.Testing
Every collector this touches is now covered by unit tests that run without a cluster: a new
hack/cozyreport.bats(166 tests) and an extendedhack/capture-previous-logs.bats(34), driven throughhack/cozytest.sh. Tenhack/*.batsfiles run green locally, 265 tests in total, with each file's@testcount reconciled against its passing count rather than trusted to the exit status — that reconciliation is the point of the trap change, since an exit code answers "did anything fail" and never "did anything run".The guard test that bans
EXIT-trap cleanup was checked by committing the violation rather than by reading it: a foldedtrap 'rm -rf "$tmp"' \+EXITadded to a file on its list turns the guard red and names that file, and removing it turns the guard green again.shellcheck --shell=shover the three changed scripts reports strictly fewer findings than before the change (hack/cozyreport.shSC2086 170 to 116, SC2162 15 to 13; the three SC2102 inhack/cozyreport-summary.share gone), andhack/e2e-capture-previous-logs.shis clean.The parts that need a cluster — that the bounds fire, and that each one lands in the artifact — are exercised by the e2e run itself, since these collectors run on every failed suite.
Screenshots
Not a UI change.
Downstream repositories
Walked the trigger map in
docs/agents/contributing.mdagainst the diff. This PR toucheshack/cozyreport.sh,hack/cozyreport-summary.sh,hack/e2e-capture-previous-logs.sh,hack/e2e-chainsaw/.chainsaw.yaml, tenhack/*.batsfiles anddocs/agents/e2e-testing.md. Nothing is moved or renamed underhack/, no make target changes what it does, and the files downstream consumers key on (hack/package.mk,hack/common-envs.mk) are untouched. No package, values key, CRD, variant, namespace or release asset is involved.Release note
Summary by CodeRabbit
New Features
Documentation
Tests