Files
cozystack/hack/cozyreport-talos.bats
Aleksei Sviridkin 7e2db4da7e fix(e2e): bound every remaining read in the report collector
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>
2026-08-07 14:38:29 +03:00

85 lines
3.8 KiB
Bash

#!/usr/bin/env bats
# Regression coverage for host-node Talos diagnostics in hack/cozyreport.sh.
# The full report needs a cluster; this test sources only the focused helper and
# replaces talosctl with a shell function that records the exact argument vector.
HACK_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")" && pwd)"
SCRIPT="$HACK_DIR/cozyreport.sh"
COZYREPORT_LIB=1
# shellcheck source=/dev/null
. "$SCRIPT"
# A PATH stub, not a shell function. These reads now go through the bounded
# reader, which runs them under `timeout` -- an external binary that execs its
# argument and therefore cannot see a function defined in this shell. A function
# mock is not merely bypassed, it is bypassed SILENTLY: the real talosctl runs, and
# what the test then reports is whatever the machine happens to have installed.
#
# cozytest.sh's parser ends an @test block at the first bare `}`, so the helper
# stays at top level rather than nested inside a test.
talos_stub_dir() {
_td=$1
mkdir -p "$_td/bin"
cat > "$_td/bin/talosctl" <<'STUB'
#!/bin/sh
printf '%s\n' "$*" >> "$TALOSCTL_CALLS"
[ -z "${TALOSCTL_FAIL:-}" ] && exit 0
echo 'error: rpc error: code = Unavailable desc = connection refused' >&2
exit 1
STUB
chmod +x "$_td/bin/talosctl"
# And a `timeout` that runs its command rather than timing it, so the assertions
# do not depend on the host having GNU coreutils.
printf '#!/bin/sh\n[ "$1" = "-k" ] && shift 2\nshift\nexec "$@"\n' > "$_td/bin/timeout"
chmod +x "$_td/bin/timeout"
COZYREPORT_BOUND="timeout -k 5 $COZYREPORT_READ_TIMEOUT"
}
@test "Talos host diagnostics pass an endpoint and use valid dmesg flags" {
# Cleanup at the end of the body, never a `trap ... EXIT`. An EXIT trap inside
# an @test replaces the one the bats binary installs for its own bookkeeping, and
# a test that then fails prints no TAP line at all -- it does not appear as
# `not ok`, it disappears, so a reader grepping the output sees a green suite.
# See docs/agents/e2e-testing.md. Both runners set `set -e`, so on a failure the
# cleanup line is never reached and the directory survives for inspection, which
# is what you want from a test that just failed.
tmp=$(mktemp -d)
talos_stub_dir "$tmp"
calls="$tmp/talosctl.calls"
TALOSCTL_CALLS="$calls" PATH="$tmp/bin:$PATH" \
cozyreport_collect_talos_node /workspace/talosconfig 192.0.2.11 "$tmp"
[ "$(sed -n '1p' "$calls")" = "--talosconfig /workspace/talosconfig -e 192.0.2.11 -n 192.0.2.11 dmesg" ]
[ "$(sed -n '2p' "$calls")" = "--talosconfig /workspace/talosconfig -e 192.0.2.11 -n 192.0.2.11 logs kubelet --tail=500" ]
[ "$(sed -n '3p' "$calls")" = "--talosconfig /workspace/talosconfig -e 192.0.2.11 -n 192.0.2.11 logs containerd --tail=500" ]
case "$(sed -n '1p' "$calls")" in *--tail*)
echo "dmesg must not receive the boolean --tail flag" >&2
false
esac
rm -rf "$tmp"
}
@test "a Talos node that refuses the connection leaves the reason in its files" {
tmp=$(mktemp -d)
talos_stub_dir "$tmp"
calls="$tmp/talosctl.calls"
TALOSCTL_FAIL=1 TALOSCTL_CALLS="$calls" PATH="$tmp/bin:$PATH" \
cozyreport_collect_talos_node /workspace/talosconfig 192.0.2.11 "$tmp"
# The reads are best-effort and must not abort the report, but "did not abort"
# and "produced nothing" are different outcomes and only one of them is about
# the node. Each read merges stderr into its own file, so a node that refused
# the connection says so where a reader looking for its dmesg will find it,
# rather than leaving three empty files that read like a quiet node.
for f in dmesg.txt kubelet.log containerd.log; do
t="$tmp/talos-192.0.2.11-$f"
[ -s "$t" ] || { echo "FAIL: $t is empty on a refused connection"; false; }
grep -q 'connection refused' "$t" || { echo "FAIL: $t does not carry the reason"; cat "$t"; false; }
done
rm -rf "$tmp"
}