Skip to content

fix(e2e): bound every read in the report collector and the step that runs it - #3596

Merged
Aleksei Sviridkin (lexfrei) merged 3 commits into
mainfrom
fix/cozyreport-bound-remaining-reads
Aug 7, 2026
Merged

fix(e2e): bound every read in the report collector and the step that runs it#3596
Aleksei Sviridkin (lexfrei) merged 3 commits into
mainfrom
fix/cozyreport-bound-remaining-reads

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What this PR does

A single hung read in hack/cozyreport.sh did not truncate cozyreport.tgz, it lost the artifact outright. The collector writes its tarball on its last line, the Collect report step carried no timeout-minutes of its own, and Upload cozyreport.tgz is the step directly after it — so a read that never returned spent whatever was left of the job budget, the job was cancelled, and the upload never ran. The runs where the report is the only evidence left are the runs that produced none. || true guards against a non-zero exit, not against a read that does not return.

Two halves, independent of each other.

The step gets a ceiling of its own, 30 minutes, in all three workflows that run it. The number is measured against the step rather than divided out of the job: pulling the Collect report timings out of recent e2e runs gives 3 to 42 seconds, on failed jobs as much as on green ones, and 30 minutes leaves five sixths of the job budget for the steps that upload what this one produced. It carries continue-on-error: true beside it, because the step's || true is its standing statement that collecting diagnostics is never fatal and a step killed at its ceiling never reaches that || true — without the tolerance, a passing run whose collection ran long would be published as failed, and every later failure() in the job would start meaning "the tests failed or the report was slow". That tolerance also renders the overrun green, so a following step turns the collect step's outcome into a ::warning:: annotation: a bound that fires is recorded, and the outer bound is not exempt from the rule the inner ones follow.

Every read in the collector is bounded, against four sections before, and gates and host commands are reads too. An if kubectl get deploy … that decides whether a module runs costs more unbounded than anything it guards, because the module below it never starts; df -h blocks indefinitely on a stale mount and takes the tarball with it by exactly the mechanism above. A gate cut off short is recorded, since skipping the module leaves the same empty tree as a cluster that never ran the component, while a gate answering "not installed" is recorded nowhere — that is the cluster answering correctly, and a marker firing on every install without KubeVirt is one a reader learns to skip.

The six LINSTOR reads run a command inside a pod and go through a reader of their own. They are the only reads whose duration a workload sets rather than the apiserver, and kubectl exec returns the remote command's exit status — a 124 there is either the collector's deadline or a command that itself exited 124, so the existing classifier would have named this script's own timeout as the cause of something that may have happened inside a pod. The new note states what the status leaves open, and sits beside the output rather than inside it, since two of the six stream a gzipped tarball. A cut-off bundle is kept rather than dropped, and the note says so: the emptiness check at those call sites asks whether the archive lists a first member, which a truncated gzip does.

What this does not do is bound the report. The pod, VM, VMI and services walks carry a count cap and a time budget; every other per-object walk carries neither, so their cost is the cluster's to set, and a failed install with a hundred not-Ready HelmReleases spends four hundred bounded reads in one of them. A per-read ceiling removes the read that never returns, not the thousand reads that each return slowly. The file header says so where it previously implied the opposite, and the step ceiling is what covers the rest.

Two frozen lists in the tests are gone, replaced by one invariant derived from the code: no read below the library guard runs without a ceiling. Both lists were already wrong. The header named four bounded sections while the file had ten, and the per-section counts of unbounded reads only ever matched a redirect into $VAR/, which no gate, selector or command substitution has — so they described the scan rather than the file.

The guard that held the old exec assumption was satisfied by leaving every exec unbounded, which is the state the file was in. It now also requires each exec to be bounded, and requires that reader not to borrow the classification written for reads whose 124 is unambiguous. Matching on the verb rather than on the string kubectl exec is part of it: half of these calls spell it kubectl -n cozy-linstor exec, which that string never saw.

Two properties worth knowing before this lands. kubectl get events -A --sort-by=.lastTimestamp sorts client-side, so kubectl buffers the whole event list and writes nothing until it finishes — under a flat per-read ceiling a cut-off there leaves a file holding only the marker, not a short tail. That read had no ceiling at all before, where the same slowness could take the whole tarball, so this is a narrower failure rather than a new one. And kubectl logs -n cozy-kamaji deployment/kamaji gains --tail=2000, matching every other controller log in the file: it was the only unbounded-in-size log, which under a wall-clock ceiling is the one most likely to be cut at its end, where a control-plane bootstrap failure sits. Longer kamaji history than 2000 lines is no longer collected.

Two accepted imperfections in how the change is split across commits, named rather than left to be found. docs/agents/e2e-testing.md keeps this material in a single shared paragraph that all three commits touch, so in the middle commit one sentence about the module gates lands a commit ahead of the gates themselves. And two corrections to the exec reader — it now keeps the remote command's whole message in the listing rather than quoting the line kubectl exec always appends last, and its cut-off note names both of the two things that can happen to a partial bundle instead of asserting one — sit in the last commit rather than in the one that introduced that reader. Both are true of the branch tip, where every statement is correct and appears once; the cost of moving them is another rebase across the one shared paragraph, which is not worth what it buys.

Refs #3568.

Screenshots

Downstream repositories

Release note

fix(e2e): every read in the diagnostic report collector now carries a wall-clock ceiling — module gates, the reads that run a command inside a LINSTOR pod, and the host commands included — and a ceiling that fires is recorded in the artifact rather than only in the job log. The workflow step that runs the collector carries a ceiling of its own, so a read that does not return costs one file and a warning annotation instead of consuming the job budget and losing cozyreport.tgz entirely.

Summary by CodeRabbit

  • New Features

    • Added bounded diagnostic collection across Kubernetes, Talos, LINSTOR, host, controller, and command outputs.
    • Reports preserve partial results and identify timeouts, failures, missing output, and truncated artifacts.
    • Added warnings when report collection fails or exceeds its time limit.
  • Bug Fixes

    • Corrected resource targets, selectors, deployment extraction, and command matching.
    • Improved handling for empty selections, unavailable resources, and missing timeout tools.
  • Documentation

    • Expanded guidance for diagnostic markers, collection limits, timeouts, and incomplete output.
  • Tests

    • Added coverage for bounded reads, failure reporting, markers, selectors, logs, and subprocess mocking.

@github-actions github-actions Bot added area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/XXL This PR changes 1000+ lines, ignoring generated files labels Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 097f0645-3a8c-4f7b-9c61-3ea4efad9fd4

📥 Commits

Reviewing files that changed from the base of the PR and between 546e33b and 7e2db4d.

📒 Files selected for processing (1)
  • hack/cozyreport.bats
🚧 Files skipped from review as they are similar to previous changes (1)
  • hack/cozyreport.bats

📝 Walkthrough

Walkthrough

The report collector now uses bounded reads across diagnostic sources, preserves partial and failed results with markers, and applies module probes. Tests validate reader behavior and workflow configuration. E2E workflows tolerate collection timeouts and emit warnings.

Changes

Diagnostic collection and failure reporting

Layer / File(s) Summary
Bounded readers and diagnostics
hack/cozyreport.sh
Adds bounded object, log, host, Talos, and pod-exec readers. The collector records timeout, truncation, stderr, empty-output, and unavailable-resource conditions.
Module collection wiring
hack/cozyreport.sh
Routes Kubernetes, Cozystack, COSI, Kamaji, LINSTOR, and sandbox-host collection through probes and bounded readers.
Collector validation
hack/cozyreport.bats, hack/cozyreport-talos.bats
Adds coverage for bounded commands, partial output, markers, selectors, probes, command failures, and executable command stubs.
Workflow timeout reporting
.github/workflows/e2e-tag.yaml, .github/workflows/nightly.yaml, .github/workflows/pull-requests.yaml, docs/agents/e2e-testing.md
Adds 30-minute report-step limits, failure tolerance, warning steps, and documentation for diagnostic markers and timeout behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels: area/ci

Suggested reviewers: myasnikovdaniil

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: bounded report-collector reads and a timeout for the workflow step that runs it.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cozyreport-bound-remaining-reads

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
hack/cozyreport.sh (1)

1661-1666: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Probe the webhook deployment separately.

Line 1661 probes the cert-manager deployment. Line 1664 then reads logs from cert-manager-webhook, which is a different deployment. If cert-manager exists and cert-manager-webhook does not, the read fails and cozyreport_read_object writes a failure record for a component that is simply absent. That is the false-alarm case cozyreport_probe was added to remove.

♻️ Proposed fix using a second probe
   if cozyreport_probe "cert-manager logs" kubectl get deploy -n cozy-cert-manager cert-manager; then
     cozyreport_read_object "$DIR/cert-manager.log" \
       kubectl logs -n cozy-cert-manager deploy/cert-manager --tail=2000
+  fi
+  if cozyreport_probe "cert-manager webhook logs" kubectl get deploy -n cozy-cert-manager cert-manager-webhook; then
     cozyreport_read_object "$DIR/cert-manager-webhook.log" \
       kubectl logs -n cozy-cert-manager deploy/cert-manager-webhook --tail=2000
   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/cozyreport.sh` around lines 1661 - 1666, Probe cert-manager and
cert-manager-webhook independently before collecting their logs. Update the
block around cozyreport_probe and cozyreport_read_object so each deployment’s
log read runs only when its corresponding probe succeeds, avoiding failure
records when one deployment is absent.
docs/agents/e2e-testing.md (1)

94-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split this bullet into several bullets or a subsection.

Line 94 is one bullet of roughly four thousand words. It covers four distinct subjects: the pod-walk count and time bounds, the LINSTOR exec reads and their archive handling, the module gates, and the workflow step ceiling with its warning annotation. A reader cannot find any one of them.

The repository guideline requires one continuous line per prose paragraph. It does not require one paragraph. Split the content into separate bullets, each still on its own single line, or promote it to a #### subsection in the style already used at line 78.

Suggested split:

  • one bullet for the pod, VM, VMI, and services walk bounds and their marker files;
  • one bullet for the LINSTOR exec reads, the table markers, and the two archive streams;
  • one bullet for the module gates;
  • one bullet for the report step timeout-minutes, continue-on-error, and the overrun warning.

The technical content is consistent with hack/cozyreport.sh and the three workflows. Only the structure needs work.

As per coding guidelines: "In Markdown prose paragraphs, use one continuous line per paragraph".

🤖 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` at line 94, Split the oversized documentation
bullet into four separate single-line Markdown bullets or a `#### ` subsection
with distinct paragraphs: pod/VM/VMI/services walk bounds and markers, LINSTOR
exec reads and archive handling, module gates, and workflow step
timeout/error-warning behavior. Preserve all existing technical content and
ensure each prose paragraph remains on one continuous line.

Source: Coding guidelines

🤖 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/cozyreport.bats`:
- Around line 620-634: Add a root-user guard to the read-only TMPDIR test around
cozyreport_read_exec, skipping the test when the suite runs with effective UID 0
because chmod-based mktemp failure is not reliable there. Preserve the existing
permission setup and assertions for non-root execution.
- Around line 1165-1170: Update the offbeat kubectl scan using fold_source so
diagnostics no longer report folded-stream line numbers; remove grep’s -n option
and print only the matching text while preserving the existing failure detection
and message flow.

In `@hack/cozyreport.sh`:
- Around line 1729-1730: Update the helm-release decoding pipeline near the
kubectl command so decoder stderr from base64 and gzip does not enter
helm-release.json; redirect it to /dev/null or an adjacent note while preserving
stdout as the JSON file and the existing failure handling.

---

Nitpick comments:
In `@docs/agents/e2e-testing.md`:
- Line 94: Split the oversized documentation bullet into four separate
single-line Markdown bullets or a `#### ` subsection with distinct paragraphs:
pod/VM/VMI/services walk bounds and markers, LINSTOR exec reads and archive
handling, module gates, and workflow step timeout/error-warning behavior.
Preserve all existing technical content and ensure each prose paragraph remains
on one continuous line.

In `@hack/cozyreport.sh`:
- Around line 1661-1666: Probe cert-manager and cert-manager-webhook
independently before collecting their logs. Update the block around
cozyreport_probe and cozyreport_read_object so each deployment’s log read runs
only when its corresponding probe succeeds, avoiding failure records when one
deployment is absent.
🪄 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: d6bdec4c-7cc7-44c9-a739-fe9925bfaab3

📥 Commits

Reviewing files that changed from the base of the PR and between 318be93 and 546e33b.

📒 Files selected for processing (7)
  • .github/workflows/e2e-tag.yaml
  • .github/workflows/nightly.yaml
  • .github/workflows/pull-requests.yaml
  • docs/agents/e2e-testing.md
  • hack/cozyreport-talos.bats
  • hack/cozyreport.bats
  • hack/cozyreport.sh

Comment thread hack/cozyreport.bats
Comment on lines +620 to +634
tmp=$(mktemp -d)
exec_stub_dir "$tmp"
ro="$tmp/readonly"; mkdir -p "$ro"; chmod 500 "$ro"

STUB_EXEC_MODE=refused TMPDIR="$ro" PATH="$tmp/bin:$PATH" \
cozyreport_read_exec "$tmp/out/pools.txt" kubectl exec -n ns deploy/c -- linstor sp l

chmod 700 "$ro"
[ -s "$tmp/out/pools.txt" ] || {
echo "FAIL: the read left nothing at all behind"; false; }
grep -q 'could not be captured' "$tmp/out/pools.txt" || {
echo "FAIL: a lost message is reported as the command having said nothing"
cat "$tmp/out/pools.txt"
false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

The read-only TMPDIR test does not hold when the suite runs as root.

Line 622 uses chmod 500 to make mktemp fail. Root ignores directory write permission, so mktemp succeeds, _cre_err is non-empty, the reader takes the stderr branch, and line 630 fails. Many CI containers run tests as root.

Add a root guard so the test skips instead of failing for a reason unrelated to the code under test.

🛡️ Proposed guard
   tmp=$(mktemp -d)
   exec_stub_dir "$tmp"
   ro="$tmp/readonly"; mkdir -p "$ro"; chmod 500 "$ro"
+  # Root ignores the write bit, so mktemp would succeed and this test would
+  # assert against a path it never reaches.
+  if [ "$(id -u)" -eq 0 ]; then rm -rf "$tmp"; return 0; 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.

Suggested change
tmp=$(mktemp -d)
exec_stub_dir "$tmp"
ro="$tmp/readonly"; mkdir -p "$ro"; chmod 500 "$ro"
STUB_EXEC_MODE=refused TMPDIR="$ro" PATH="$tmp/bin:$PATH" \
cozyreport_read_exec "$tmp/out/pools.txt" kubectl exec -n ns deploy/c -- linstor sp l
chmod 700 "$ro"
[ -s "$tmp/out/pools.txt" ] || {
echo "FAIL: the read left nothing at all behind"; false; }
grep -q 'could not be captured' "$tmp/out/pools.txt" || {
echo "FAIL: a lost message is reported as the command having said nothing"
cat "$tmp/out/pools.txt"
false
}
tmp=$(mktemp -d)
exec_stub_dir "$tmp"
ro="$tmp/readonly"; mkdir -p "$ro"; chmod 500 "$ro"
# Root ignores the write bit, so mktemp would succeed and this test would
# assert against a path it never reaches.
if [ "$(id -u)" -eq 0 ]; then rm -rf "$tmp"; return 0; fi
STUB_EXEC_MODE=refused TMPDIR="$ro" PATH="$tmp/bin:$PATH" \
cozyreport_read_exec "$tmp/out/pools.txt" kubectl exec -n ns deploy/c -- linstor sp l
chmod 700 "$ro"
[ -s "$tmp/out/pools.txt" ] || {
echo "FAIL: the read left nothing at all behind"; false; }
grep -q 'could not be captured' "$tmp/out/pools.txt" || {
echo "FAIL: a lost message is reported as the command having said nothing"
cat "$tmp/out/pools.txt"
false
}
🤖 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 620 - 634, Add a root-user guard to the
read-only TMPDIR test around cozyreport_read_exec, skipping the test when the
suite runs with effective UID 0 because chmod-based mktemp failure is not
reliable there. Preserve the existing permission setup and assertions for
non-root execution.

Comment thread hack/cozyreport.bats
Comment on lines 1165 to +1170
offbeat=$(fold_source "$SCRIPT" \
| grep -nE '(^|[[:space:]|(])(/[A-Za-z0-9_/.-]*kubectl|sudo[[:space:]]+kubectl|\$\{?KUBECTL)' || true)
if [ -n "$offbeat" ]; then
echo "FAIL: kubectl is invoked in a form this inventory cannot see:"
echo "FAIL: kubectl is invoked in a form these scans cannot see:"
printf '%s\n' "$offbeat"
echo "Either keep invoking it by bare name, or widen the scan above in the same commit."
false
fi

if [ "$inventory" != "$expected" ]; then
echo "FAIL: the inventory of unbounded per-object reads changed."
echo " expected: $expected"
echo " found: $inventory"
echo "A new section must route its reads through cozyreport_read_object;"
echo "if you bounded an existing one, update this list in the same commit."
echo "Either keep invoking it by bare name, or widen the scans in the same commit."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The reported line numbers do not match the source file.

fold_source removes comment lines and joins continuations, so grep -n numbers the folded stream. When this guard fires, the printed numbers point at nothing in hack/cozyreport.sh, which sends the reader to the wrong place.

Drop -n and print the matched text only, or resolve the offending spelling against the unfolded file.

🔧 Proposed change
   offbeat=$(fold_source "$SCRIPT" \
-    | grep -nE '(^|[[:space:]|(])(/[A-Za-z0-9_/.-]*kubectl|sudo[[:space:]]+kubectl|\$\{?KUBECTL)' || true)
+    | grep -E '(^|[[:space:]|(])(/[A-Za-z0-9_/.-]*kubectl|sudo[[:space:]]+kubectl|\$\{?KUBECTL)' || true)
📝 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.

Suggested change
offbeat=$(fold_source "$SCRIPT" \
| grep -nE '(^|[[:space:]|(])(/[A-Za-z0-9_/.-]*kubectl|sudo[[:space:]]+kubectl|\$\{?KUBECTL)' || true)
if [ -n "$offbeat" ]; then
echo "FAIL: kubectl is invoked in a form this inventory cannot see:"
echo "FAIL: kubectl is invoked in a form these scans cannot see:"
printf '%s\n' "$offbeat"
echo "Either keep invoking it by bare name, or widen the scan above in the same commit."
false
fi
if [ "$inventory" != "$expected" ]; then
echo "FAIL: the inventory of unbounded per-object reads changed."
echo " expected: $expected"
echo " found: $inventory"
echo "A new section must route its reads through cozyreport_read_object;"
echo "if you bounded an existing one, update this list in the same commit."
echo "Either keep invoking it by bare name, or widen the scans in the same commit."
offbeat=$(fold_source "$SCRIPT" \
| grep -E '(^|[[:space:]|(])(/[A-Za-z0-9_/.-]*kubectl|sudo[[:space:]]+kubectl|\$\{?KUBECTL)' || true)
if [ -n "$offbeat" ]; then
echo "FAIL: kubectl is invoked in a form these scans cannot see:"
printf '%s\n' "$offbeat"
echo "Either keep invoking it by bare name, or widen the scans in the same commit."
🤖 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 1165 - 1170, Update the offbeat kubectl
scan using fold_source so diagnostics no longer report folded-stream line
numbers; remove grep’s -n option and print only the matching text while
preserving the existing failure detection and message flow.

Comment thread hack/cozyreport.sh
Comment on lines +1729 to 1730
$COZYREPORT_BOUND kubectl get secret -n $NAMESPACE $SECRET -o jsonpath='{.data.release}' 2>/dev/null \
| base64 -d | base64 -d | gzip -d > $DIR/helm-release.json 2>&1 || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep decoder stderr out of helm-release.json.

Line 1730 redirects stdout to $DIR/helm-release.json and then applies 2>&1. Stderr from base64 and gzip therefore lands inside the JSON file. A failed decode produces a file that looks like release data but cannot be parsed, and nothing marks it. This is the same parser-corruption case cozyreport_read_object avoids for YAML targets at line 693.

Send the decoder stderr to /dev/null or to a note beside the file.

🐛 Proposed fix
         $COZYREPORT_BOUND kubectl get secret -n $NAMESPACE $SECRET -o jsonpath='{.data.release}' 2>/dev/null \
-          | base64 -d | base64 -d | gzip -d > $DIR/helm-release.json 2>&1 || true
+          | base64 -d 2>/dev/null | base64 -d 2>/dev/null | gzip -d > $DIR/helm-release.json 2>/dev/null || true
📝 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.

Suggested change
$COZYREPORT_BOUND kubectl get secret -n $NAMESPACE $SECRET -o jsonpath='{.data.release}' 2>/dev/null \
| base64 -d | base64 -d | gzip -d > $DIR/helm-release.json 2>&1 || true
$COZYREPORT_BOUND kubectl get secret -n $NAMESPACE $SECRET -o jsonpath='{.data.release}' 2>/dev/null \
| base64 -d 2>/dev/null | base64 -d 2>/dev/null | gzip -d > $DIR/helm-release.json 2>/dev/null || 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.sh` around lines 1729 - 1730, Update the helm-release
decoding pipeline near the kubectl command so decoder stderr from base64 and
gzip does not enter helm-release.json; redirect it to /dev/null or an adjacent
note while preserving stdout as the JSON file and the existing failure handling.

@lexfrei

Copy link
Copy Markdown
Contributor Author

Reopening to pick up the multus fix on main.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Reviewed the whole collector: every kubectl/talosctl/host read goes through a reader or the $COZYREPORT_BOUND wrapper — no unbounded hanging read remains. cozyreport-summary.sh (invoked before the tarball write, not in the diff) was checked separately and has no gap either. Partial results are kept and marked, collection is strictly sequential (no races), temp cleanup is correct, and the three workflows are patched consistently. 170 bats tests green; key invariants confirmed non-vacuous by mutation.

Non-blocking notes:

  • The branch is behind main, so the frozen-list EXIT-traps test fails on the branch head. This is not a PR defect — main already added the file to the list in a commit the branch lacks, and the merge will pull it in automatically. A rebase on main would make the head green on its own.
  • Very dense essay-style comments (~2069 lines); the template-comment-bloat rule doesn't apply (shell, not a chart template), but the maintenance cost is real.

The step that runs hack/cozyreport.sh carried no timeout-minutes in any
of the three workflows that run it, so it inherited only the job's 180.
The collector writes its tarball on its last line and the upload is the
next step, so a read that never returns spends what is left of the job
budget, the job is cancelled, and the upload never runs -- the artifact
is lost entirely rather than truncated, on exactly the run where it is
the only evidence left. `|| true` guards against a non-zero exit, not
against a read that does not return.

30 minutes is measured against the step rather than divided out of the
job: it takes 3-42s across sampled runs, on failed e2e jobs as well as
green ones, and 30 leaves five sixths of the job budget for the steps
that upload what this one produced.

A test pins the ceiling in all three workflows and requires it to stay
under the job's own, since a step ceiling at or above the job's cap is
the same as no ceiling at all.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
The LINSTOR module runs six reads through `kubectl exec`: three
listings, the error-report index, and the controller and per-satellite
ErrorReport bundles. They are the only reads in the collector whose
duration is set by a workload rather than by the apiserver -- a wedged
controller, a satellite that stops answering, or a broken SPDY upgrade
holds the stream open with no clock anywhere in the path. The tarball is
written on the collector's last line, so one of those never returning
does not truncate the report, it loses it.

They go through a reader of their own rather than the existing one.
`kubectl exec` returns the remote command's exit status, so a 124 there
is either this collector's deadline expiring or a command that itself
exited 124, and the status does not separate them; describing it the way
every `get`, `logs` and `describe` is described would state a mechanism
about the machine writing the report for something that may have
happened inside a pod. The note says what is open instead of resolving
it, and goes beside the output rather than into it, since two of the six
stream a gzipped tarball that a line of prose turns into an archive
nothing can open. kubectl's own message travels with the note for the
same reason -- until now it was merged into the listings, so every run
put the "Defaulted container" line at the top of a table.

The guard that held the old assumption asserted only that no bounded
read was an exec, which leaving every exec unbounded satisfies. It now
also requires each exec to be bounded and requires the reader not to
borrow the classification written for reads whose 124 is unambiguous.
Matching on the verb rather than on the string `kubectl exec` is part of
the fix: half of these calls spell it `kubectl -n cozy-linstor exec`,
which that string never saw.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Four sections of hack/cozyreport.sh read with a wall-clock ceiling and
the rest -- nodes, namespaces, events, HelmReleases, packages,
packagesources, PVCs, cert-manager, COSI, kamaji, the Talos node reads
and every module gate -- read with none. The tarball is written on the
collector's last line, so a single read that never returns does not
truncate the report, it loses it, on the run where it is the only
evidence left.

Each read now goes through the shape that fits it: the existing bounded
reader for a read whose output is a file, the existing selector for one
that feeds a loop, and a new probe for the `if kubectl get deploy ...`
gates. A gate is a read like any other, and an unbounded one is worse
than the reads it guards, since the module below it never starts. Its
cut-off is recorded, because skipping the module leaves the same empty
tree as a cluster that never had the component; a gate that answers "not
installed" is recorded nowhere, since that is the cluster answering
correctly and a marker firing on every install without KubeVirt is one a
reader learns to skip. The reads that stay on the bare wrapper say so
where they sit; what they share is that the output is not the reader's
to write, going into a variable or through a pipeline that transforms
it, so there is no moment at which a marker could attach.

The header claimed a per-section list of what was bounded, and the tests
froze that list plus a per-section count of the unbounded reads. Both are
gone. The list had already gone stale, naming four sections while the
file had ten, and the counts were an undercount by construction -- they
matched a redirect to `$VAR/`, which no gate, selector or command
substitution has. One test replaces both: no read below the library guard
runs without a ceiling, checked by looking for a counterexample.

A ceiling per read is not a ceiling on the report, and the header now
says so where it used to imply the opposite. The pod, VM, VMI and
services walks carry a count cap and a time budget; the walks over nodes,
namespaces, HelmReleases, packages, PVCs and certificates carry neither,
so their cost is the cluster's to set. What this removes is the read that
never returns.

The bounded reader now names the tool it ran and describes what that tool
returned. It served kubectl alone until this change gave it talosctl and
the four host commands, so a `df` that hit a stale mount left a marker
reading "kubectl exited 1 ... not because the object ended" -- two false
statements in one line, in an artifact whose premise is that its notes
can be believed.

The exec reader keeps the remote command's whole message in the listing
it belongs to. `kubectl exec` appends its own `command terminated with
exit code N` after whatever the command said, so quoting the last line
there drops the cause and keeps the trailer, and sending the message
away from the file leaves a zero-byte listing -- the "returned nothing,
or never ran" question this collector exists to answer. Its cut-off note
also stops asserting that the archive is beside it: a stream that got
members across is kept and truncated, one killed before its first byte
leaves zero bytes and the call site's emptiness check removes it, and
the reader runs before that check, so it names both.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants