mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-09 05:25:14 +09:00
The shell translator rewrites test bodies and loses Bats reporting. The unit lane needs complete TAP results and JUnit reports without changing how live-cluster suites run. Sourced production helpers still need a dash pass, and unit test bodies need nounset without exporting shell options into subprocesses. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
1630 lines
66 KiB
Bash
1630 lines
66 KiB
Bash
#!/usr/bin/env bats
|
|
# Regression coverage for tenant-worker guest serial console capture in
|
|
# hack/e2e-chainsaw/_lib/run-kubernetes.sh. cozytest.sh ends an @test block at
|
|
# the first bare closing brace, so command mocks stay at top level.
|
|
#
|
|
# The capture exists for the one failure class no other collector reaches: a
|
|
# worker that stalls in the guest before Talos apid answers. Its defining
|
|
# property is therefore what it does NOT need — no talosctl, no reader
|
|
# certificate, no helper Pod, nothing inside the guest. These tests pin that
|
|
# property, because a capture that grew an in-guest dependency would still look
|
|
# healthy while being blind to exactly the runs it was written for.
|
|
|
|
load test_helper
|
|
|
|
kubectl_calls=/dev/null
|
|
kubectl_pod_names=
|
|
kubectl_list_rc=0
|
|
kubectl_list_stderr=
|
|
kubectl_init_names=
|
|
kubectl_init_rc=0
|
|
kubectl_wedge_rows=
|
|
kubectl_wedge_rc=0
|
|
kubectl_wedge_stderr=
|
|
mktemp_fail=0
|
|
kubectl_logs_output='mock console output'
|
|
kubectl_logs_rc=0
|
|
kubectl_logs_stderr=
|
|
kubectl_timeline_rows='name=virt-launcher-worker-a-11111 phase=Running podStartTime=2026-08-15T14:35:26Z computeStartedAt=2026-08-15T14:35:47Z consoleStartedAt=2026-08-15T14:35:47Z'
|
|
kubectl_timeline_rc=0
|
|
kubectl_timeline_stderr=
|
|
talosctl_calls=/dev/null
|
|
timeout_calls=/dev/null
|
|
timeout_fail_pod=
|
|
date_now=
|
|
|
|
date() {
|
|
# Same shape the node-join suite uses: a fixed clock so the passing path's
|
|
# remaining-time arithmetic is decided by the test rather than by when it ran.
|
|
if [ -n "${date_now}" ] && [ "${1:-}" = +%s ]; then
|
|
printf '%s\n' "${date_now}"
|
|
return 0
|
|
fi
|
|
command date "$@"
|
|
}
|
|
|
|
kubectl() {
|
|
printf '%s\n' "$*" >>"${kubectl_calls}"
|
|
|
|
if [ "${3:-}" = get ] && [ "${4:-}" = pods ]; then
|
|
case "$*" in
|
|
*computeStartedAt*)
|
|
# Rows first, status after, so a cut-off read can be modelled as what it
|
|
# is: a prefix that arrived plus a non-zero status. Returning early on
|
|
# the status instead would make "failed" and "returned nothing" the same
|
|
# scenario, and the branch that tells them apart untestable.
|
|
#
|
|
# stderr is staged separately for the same reason every other arm here
|
|
# stages it: kubectl writes warnings beside a healthy read as well as
|
|
# errors beside a failed one, and which file those land in is decided by
|
|
# the status rather than by anything in the text.
|
|
[ -z "${kubectl_timeline_stderr}" ] || printf '%s\n' "${kubectl_timeline_stderr}" >&2
|
|
[ -z "${kubectl_timeline_rows}" ] || printf '%s\n' "${kubectl_timeline_rows}"
|
|
return "${kubectl_timeline_rc}"
|
|
;;
|
|
*initContainerStatuses*)
|
|
[ -z "${kubectl_wedge_stderr}" ] || printf '%s\n' "${kubectl_wedge_stderr}" >&2
|
|
[ "${kubectl_wedge_rc}" -eq 0 ] || return "${kubectl_wedge_rc}"
|
|
[ -z "${kubectl_wedge_rows}" ] || printf '%s\n' "${kubectl_wedge_rows}"
|
|
return 0
|
|
;;
|
|
*initContainers*)
|
|
[ "${kubectl_init_rc}" -eq 0 ] || return "${kubectl_init_rc}"
|
|
[ -z "${kubectl_init_names}" ] || printf '%s\n' "${kubectl_init_names}"
|
|
return 0
|
|
;;
|
|
esac
|
|
[ -z "${kubectl_list_stderr}" ] || printf '%s\n' "${kubectl_list_stderr}" >&2
|
|
[ "${kubectl_list_rc}" -eq 0 ] || return "${kubectl_list_rc}"
|
|
[ -z "${kubectl_pod_names}" ] || printf '%s\n' ${kubectl_pod_names}
|
|
return 0
|
|
fi
|
|
|
|
if [ "${3:-}" = logs ]; then
|
|
[ -z "${kubectl_logs_stderr}" ] || printf '%s\n' "${kubectl_logs_stderr}" >&2
|
|
[ "${kubectl_logs_rc}" -eq 0 ] || return "${kubectl_logs_rc}"
|
|
[ -z "${kubectl_logs_output}" ] || printf '%s for %s\n' "${kubectl_logs_output}" "${4:-}"
|
|
return 0
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
talosctl() {
|
|
printf '%s\n' "$*" >>"${talosctl_calls}"
|
|
}
|
|
|
|
timeout() {
|
|
local command_rc=0
|
|
printf '%s\n' "$*" >>"${timeout_calls}"
|
|
[ "${1:-}" = -k ] || return 97
|
|
shift 3
|
|
"$@" || command_rc=$?
|
|
if [ -n "${timeout_fail_pod}" ]; then
|
|
case " $* " in
|
|
*" ${timeout_fail_pod} "*) return 124 ;;
|
|
esac
|
|
fi
|
|
return "${command_rc}"
|
|
}
|
|
|
|
mktemp() {
|
|
# The scratch file the wedge check allocates is the one failure the function
|
|
# cannot report through kubectl, so it needs its own knob. `-d` is left
|
|
# working because the tests themselves use it for their own temp dirs.
|
|
if [ "${mktemp_fail}" = 1 ] && [ "${1:-}" != -d ]; then
|
|
return 1
|
|
fi
|
|
command mktemp "$@"
|
|
}
|
|
|
|
assert_file_contains() {
|
|
local needle="$1"
|
|
local file="$2"
|
|
|
|
case "$(cat "${file}")" in
|
|
*"${needle}"*) return 0 ;;
|
|
esac
|
|
printf 'expected %s to contain: %s\n' "${file}" "${needle}" >&2
|
|
return 1
|
|
}
|
|
|
|
assert_file_lacks_pattern() {
|
|
local pattern="$1"
|
|
local file="$2"
|
|
|
|
# A missing file must fail rather than vacuously pass: awk exits 2 on an
|
|
# unreadable path, which is indistinguishable from "no line matched" once the
|
|
# status is folded into an if.
|
|
if [ ! -f "${file}" ]; then
|
|
printf 'expected %s to exist so it could be checked for: %s\n' "${file}" "${pattern}" >&2
|
|
return 1
|
|
fi
|
|
if awk -v pattern="${pattern}" '$0 ~ pattern { found = 1 } END { exit found ? 0 : 1 }' "${file}"; then
|
|
printf 'expected %s not to match: %s\n' "${file}" "${pattern}" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# The function under test redirects a command's stderr into a report file and
|
|
# decides which artifact to write from it. cozytest.sh runs under `set -x`, so
|
|
# the tracer's own line for that command is emitted on the very stderr being
|
|
# redirected, and every assertion about those files would be reading the
|
|
# tracer. Call through this wrapper so the sinks hold what production would
|
|
# put in them.
|
|
run_capture() {
|
|
local _rc=0
|
|
( set +x; cozy_capture_tenant_serial_console "${1:-capture staged by a unit test}" "${2:-6}" ) || _rc=$?
|
|
return "${_rc}"
|
|
}
|
|
|
|
@test "a wedged console container is named in the headline, not buried" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false PodInitializing virt-launcher-worker-a-11111\nfalse PodInitializing virt-launcher-worker-b-22222')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# If kubevirt/kubevirt#15989 still bites, the suite's own headline reads
|
|
# "fewer than 2 tenant nodes Ready within 29m" -- byte-identical to the
|
|
# failure this instrumentation exists to study. A triager would file the
|
|
# experiment's answer as the known flake. The diagnostic has to name its
|
|
# own worst case before the noise starts.
|
|
assert_file_contains 'guest-console-log' "$tmp/out"
|
|
assert_file_contains '15989' "$tmp/out"
|
|
assert_file_contains 'virt-launcher-worker-a-11111' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a read that never answered is not reported as a healthy console" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rc=1
|
|
kubectl_wedge_rows="$(printf 'false PodInitializing virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# The guard against a false headline must not become a false all-clear.
|
|
# An apiserver blip here would otherwise leave the reader with no line at
|
|
# all, which reads as "the console container is fine" -- nothing was
|
|
# checked. Same rule the sibling attach check follows.
|
|
assert_file_lacks_pattern '15989' "$tmp/out"
|
|
assert_file_contains 'unknown, not fine' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the wedge row carries the waiting reason, not just the ready bit" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false PodInitializing virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# ready=false on a sidecar means "not running", which a terminated Pod
|
|
# satisfies as well as a wedged one. The reason keeps the two apart.
|
|
assert_file_contains 'PodInitializing' "$tmp/out"
|
|
assert_file_contains 'state.waiting.reason' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the taxonomy does not claim to be exhaustive" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false OOMKilled virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# Two named reasons do not cover the space -- OOMKilled and ContainerCreating
|
|
# reach this line too, and a superseded Pod killed past its grace period
|
|
# reads Error. A headline that enumerates two and stops implies the rest
|
|
# cannot happen, which is the over-claim this whole line was rewritten to
|
|
# stop making.
|
|
assert_file_contains 'any other reason, or none, is not covered' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the wedge taxonomy names CrashLoopBackOff as the bug, not as a superseded Pod" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false CrashLoopBackOff virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# The sidecar restarts on failure, so the hang settles into CrashLoopBackOff
|
|
# and passes through Error. Filing those under "superseded Pod" would hand a
|
|
# triager the experiment's own failure labelled as noise -- the precise
|
|
# inversion this function exists to prevent.
|
|
assert_file_contains 'CrashLoopBackOff or Error below is the wedge shape' "$tmp/out"
|
|
assert_file_lacks_pattern 'Completed or Error below means a superseded' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a superseded Pod is labelled, not called a wedge" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false Completed virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# ContainerState is a union, so a waiting reason alone is blank for the
|
|
# terminated case and cannot separate it from anything else at ready=false.
|
|
# The terminated reason is what labels the superseded Pod, and Completed is
|
|
# the shape that means superseded rather than wedged.
|
|
assert_file_contains 'Completed' "$tmp/out"
|
|
assert_file_contains 'state.terminated.reason' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the headline points at the describe rather than concluding" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# The bit cannot separate every shape, so the line must not assert one. A
|
|
# headline that concludes more than it established is the same mislabel this
|
|
# function exists to prevent, pointing the other way.
|
|
assert_file_contains 'if these are current Pods' "$tmp/out"
|
|
assert_file_contains 'describe that follows settles it' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a scratch file that could not be allocated is not silence either" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'false CrashLoopBackOff virt-launcher-worker-a-11111')"
|
|
mktemp_fail=1
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
mktemp_fail=0
|
|
|
|
# The function argues twelve lines further down that silence would read as
|
|
# "the console container is fine" when nothing was checked. That argument
|
|
# does not stop applying one branch earlier.
|
|
assert_file_contains 'unknown, not fine' "$tmp/out"
|
|
assert_file_lacks_pattern '15989' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a failed read keeps the reason it failed" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rc=1
|
|
kubectl_wedge_stderr="Error from server: etcdserver: leader changed"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# Discarding stderr here would report a bare exit code for a failure whose
|
|
# cause kubectl already named, on the one path that exists to stop a reader
|
|
# guessing. Same rule the sibling attach check follows.
|
|
assert_file_contains 'leader changed' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a healthy console container says nothing in the headline" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_wedge_rows="$(printf 'true virt-launcher-worker-a-11111')"
|
|
|
|
( set +x; cozy_report_guest_console_wedge ) >"$tmp/out" 2>&1
|
|
|
|
# A marker that fires on healthy runs is one a reader learns to skip.
|
|
assert_file_lacks_pattern '15989' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the wedge check runs before the rest of the node-join diagnostics" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
head=$(grep -n 'node-join failed: fewer than 2 tenant nodes Ready' "$lib" | head -n 1 | cut -d: -f1)
|
|
wedge=$(grep -n '^ *cozy_report_guest_console_wedge || true$' "$lib" | head -n 1 | cut -d: -f1)
|
|
first=$(grep -n 'describe nodes' "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$wedge" ]; then
|
|
echo "expected the node-join failure path to check for a wedged console container" >&2
|
|
return 1
|
|
fi
|
|
if [ "$wedge" -le "$head" ] || [ "$wedge" -ge "$first" ]; then
|
|
echo "the wedge check (line $wedge) must sit between the headline ($head) and the first diagnostic ($first)" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "console capture needs nothing from inside the guest" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
talosctl_calls="$tmp/talosctl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-smoke
|
|
|
|
run_capture
|
|
|
|
assert_file_contains 'logs virt-launcher-worker-a-11111 -c guest-console-log' "$kubectl_calls"
|
|
assert_file_lacks_pattern 'certificate|exec|apply|talosconfig' "$kubectl_calls"
|
|
[ ! -s "$talosctl_calls" ]
|
|
assert_file_contains 'mock console output' "$COZY_REPORT_DIR/snapshots/console-smoke/tenant-serial-console/virt-launcher-worker-a-11111.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "each read is bounded and keeps its exit code" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-bounded
|
|
|
|
run_capture
|
|
|
|
assert_file_contains '-k 5 30 kubectl -n tenant-test logs virt-launcher-worker-a-11111 -c guest-console-log' "$timeout_calls"
|
|
assert_file_contains '[capture exit code: 0]' "$COZY_REPORT_DIR/snapshots/console-bounded/tenant-serial-console/virt-launcher-worker-a-11111.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the read keeps the boot output instead of the tail" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-head
|
|
|
|
run_capture
|
|
|
|
# A guest that stalls before apid is diagnosed from what it printed while
|
|
# booting, and a guest that instead repaints the console indefinitely would
|
|
# push that boot output out of any --tail window. --limit-bytes truncates
|
|
# from the far end instead, which keeps whatever the kubelet still holds.
|
|
# It cannot recover what container-log rotation already dropped.
|
|
assert_file_contains '--limit-bytes=' "$kubectl_calls"
|
|
assert_file_lacks_pattern '--tail' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a pod whose read times out does not stop the next pod" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111 virt-launcher-worker-b-22222"
|
|
timeout_fail_pod=virt-launcher-worker-a-11111
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-partial
|
|
|
|
run_capture
|
|
|
|
assert_file_contains '[capture exit code: 124]' "$COZY_REPORT_DIR/snapshots/console-partial/tenant-serial-console/virt-launcher-worker-a-11111.log"
|
|
assert_file_contains '[capture exit code: 0]' "$COZY_REPORT_DIR/snapshots/console-partial/tenant-serial-console/virt-launcher-worker-b-22222.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "an empty virt-launcher list is recorded rather than passed over" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-empty
|
|
|
|
capture_rc=0
|
|
run_capture || capture_rc=$?
|
|
|
|
[ "$capture_rc" -eq 1 ]
|
|
assert_file_contains 'no virt-launcher Pod found' "$COZY_REPORT_DIR/snapshots/console-empty/tenant-serial-console/setup-error.log"
|
|
assert_file_lacks_pattern 'guest-console-log' "$timeout_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a failed Pod listing is recorded rather than passed over" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_list_rc=1
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-list-error
|
|
|
|
capture_rc=0
|
|
run_capture || capture_rc=$?
|
|
|
|
[ "$capture_rc" -eq 1 ]
|
|
assert_file_contains 'failed to list tenant virt-launcher Pods' "$COZY_REPORT_DIR/snapshots/console-list-error/tenant-serial-console/setup-error.log"
|
|
assert_file_lacks_pattern 'guest-console-log' "$timeout_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a warning on a successful listing is kept apart from a setup failure" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_list_stderr="W0807 partial results warning"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-warn
|
|
|
|
run_capture
|
|
|
|
# kubectl writes to stderr and exits 0 for warnings and for "No resources
|
|
# found" alike, so the exit status decides the file. A warning filed as a
|
|
# setup error reads as a broken collector on an otherwise healthy run.
|
|
assert_file_contains 'partial results warning' "$COZY_REPORT_DIR/snapshots/console-warn/tenant-serial-console/READ-WARNINGS.txt"
|
|
[ ! -f "$COZY_REPORT_DIR/snapshots/console-warn/tenant-serial-console/setup-error.log" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a healthy listing leaves no warnings file behind" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-quiet
|
|
|
|
run_capture
|
|
|
|
[ ! -f "$COZY_REPORT_DIR/snapshots/console-quiet/tenant-serial-console/READ-WARNINGS.txt" ]
|
|
[ ! -f "$COZY_REPORT_DIR/snapshots/console-quiet/tenant-serial-console/setup-error.log" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a failed listing keeps the stderr that explains it" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_list_rc=1
|
|
kubectl_list_stderr="Error from server (Forbidden): pods is forbidden"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-forbidden
|
|
|
|
capture_rc=0
|
|
run_capture || capture_rc=$?
|
|
|
|
[ "$capture_rc" -eq 1 ]
|
|
assert_file_contains 'Forbidden' "$COZY_REPORT_DIR/snapshots/console-forbidden/tenant-serial-console/setup-error.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the attach check passes when the console container is present" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="virt-launcher-worker-a-11111=guest-console-log"
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
|
|
|
|
[ "$attach_rc" -eq 0 ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a matched Pod with no init containers at all is the finding, not a miss" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="virt-launcher-worker-a-11111="
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
|
|
|
|
# This is the shape an inert setting actually produces here: guest-console-log
|
|
# is the only init container these Pods ever get, so "the field did not take
|
|
# effect" means initContainers is absent, not that some other name appears.
|
|
# Reading a bare list of container names would render it byte-identical to
|
|
# "no Pod matched" and let the suite pass over the one thing this checks.
|
|
[ "$attach_rc" -eq 2 ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a read that never answered is not reported as an inert setting" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_rc=1
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) 2>"$tmp/err" || attach_rc=$?
|
|
|
|
# Piping the read into grep would collapse this onto the absent-container
|
|
# branch: no output, grep fails, and an API blip gets published as "the
|
|
# override did not take effect" -- a cause nothing established. The message
|
|
# is asserted, not just the code: a failed read and a read that matched
|
|
# nothing share an exit code but are different facts, and only the status
|
|
# tells them apart.
|
|
[ "$attach_rc" -eq 1 ]
|
|
assert_file_contains 'could not read tenant virt-launcher Pods' "$tmp/err"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a read that matched no Pod is not reported as an inert setting either" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names=
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) 2>"$tmp/err" || attach_rc=$?
|
|
|
|
# A selector that matched nothing says nothing about any Pod's containers,
|
|
# so it belongs with the failed read rather than with the absent container.
|
|
[ "$attach_rc" -eq 1 ]
|
|
assert_file_contains 'no tenant virt-launcher Pod matched' "$tmp/err"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the absent-container diagnostics are bounded like the check itself" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="virt-launcher-worker-a-11111="
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
|
|
|
|
[ "$attach_rc" -eq 2 ]
|
|
# These run on a path that ends in exit 1; an unbounded hang here would take
|
|
# the step's deadline and the tenant snapshot the exit is meant to reach.
|
|
[ "$(grep -c 'get virtualmachineinstances' "$timeout_calls")" -eq 1 ]
|
|
[ "$(grep -c 'get pods' "$timeout_calls")" -eq 2 ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a silent read records whether the container ever started" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_logs_output=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-silent
|
|
|
|
run_capture
|
|
|
|
# An empty console log has two causes that look identical in the artifact:
|
|
# the guest printed nothing, or the container never started so there was no
|
|
# stream at all. The second is what kubevirt/kubevirt#15989 produces, and it
|
|
# is the reading that would otherwise be filed as "the guest was silent".
|
|
# The Pod's own state is the only thing that separates them.
|
|
d="$COZY_REPORT_DIR/snapshots/console-silent/tenant-serial-console"
|
|
assert_file_contains 'describe pod' "$d/POD-STATE.txt"
|
|
assert_file_contains 'events' "$d/POD-STATE.txt"
|
|
assert_file_contains 'returned nothing' "$d/virt-launcher-worker-a-11111.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a container that never started is not called truncated or silent" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_logs_output=
|
|
kubectl_logs_rc=1
|
|
kubectl_logs_stderr="Error from server (BadRequest): container guest-console-log is not valid for pod virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-absent
|
|
|
|
run_capture
|
|
|
|
# This is the shape kubevirt/kubevirt#15989 produces and the headline case
|
|
# for the whole collector. Folding stderr into the capture would make the
|
|
# file non-empty and the console would read as having said something;
|
|
# nothing was truncated, because nothing was ever read.
|
|
d="$COZY_REPORT_DIR/snapshots/console-absent/tenant-serial-console"
|
|
assert_file_contains 'no console at all' "$d/virt-launcher-worker-a-11111.log"
|
|
assert_file_lacks_pattern 'truncated' "$d/virt-launcher-worker-a-11111.log"
|
|
assert_file_contains 'is not valid for pod' "$d/virt-launcher-worker-a-11111.read-error.log"
|
|
[ -f "$d/POD-STATE.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a read killed without a word does not point at a missing file" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_logs_output=
|
|
timeout_fail_pod=virt-launcher-worker-a-11111
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-killed
|
|
|
|
run_capture
|
|
|
|
# timeout kills the child without a word, so both streams can be empty.
|
|
# Naming read-error.log then sends the reader after evidence that was
|
|
# never written.
|
|
d="$COZY_REPORT_DIR/snapshots/console-killed/tenant-serial-console"
|
|
[ ! -f "$d/virt-launcher-worker-a-11111.read-error.log" ]
|
|
assert_file_contains 'failed silently' "$d/virt-launcher-worker-a-11111.log"
|
|
assert_file_lacks_pattern 'see read-error.log' "$d/virt-launcher-worker-a-11111.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a warning beside a healthy capture is not filed as a read error" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_logs_stderr="Warning: deprecated flag"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-podwarn
|
|
|
|
run_capture
|
|
|
|
# Same rule the Pod-list read follows: kubectl writes warnings on stderr
|
|
# with a zero status, so presence of stderr does not make it an error.
|
|
d="$COZY_REPORT_DIR/snapshots/console-podwarn/tenant-serial-console"
|
|
assert_file_contains 'deprecated flag' "$d/virt-launcher-worker-a-11111.READ-WARNINGS.txt"
|
|
[ ! -f "$d/virt-launcher-worker-a-11111.read-error.log" ]
|
|
[ ! -f "$d/POD-STATE.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a healthy read leaves no read-error file" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-noerr
|
|
|
|
run_capture
|
|
|
|
[ ! -f "$COZY_REPORT_DIR/snapshots/console-noerr/tenant-serial-console/virt-launcher-worker-a-11111.read-error.log" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a truncated read is not reported as silence" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
timeout_fail_pod=virt-launcher-worker-a-11111
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-truncated
|
|
|
|
out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 6 ) 2>/dev/null )
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
|
|
# The read broke off mid-stream, so the file holds a prefix. Calling that
|
|
# "no console output" is the same unestablished claim about silence the
|
|
# Pod-state capture exists to prevent.
|
|
f="$COZY_REPORT_DIR/snapshots/console-truncated/tenant-serial-console/virt-launcher-worker-a-11111.log"
|
|
assert_file_contains 'mock console output' "$f"
|
|
assert_file_contains 'truncated' "$f"
|
|
assert_file_lacks_pattern 'no console output' "$f"
|
|
# And the summary line the passing path reads must not undo that. It is fed
|
|
# by the same counter that gates the Pod-state read, which a cut-short console
|
|
# needs as much as an empty one -- so counting them together is right and
|
|
# naming them together is not. On the green path this line is the only thing
|
|
# anyone sees, and a partial baseline reported as no baseline sends the reader
|
|
# past evidence that is sitting in the artifact.
|
|
assert_file_contains 'of the 1 guest consoles read, 0 came back empty and 1 were cut short' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a failed read records the Pod state too" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
timeout_fail_pod=virt-launcher-worker-a-11111
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-failed-read
|
|
|
|
run_capture
|
|
|
|
assert_file_contains 'describe pod' "$COZY_REPORT_DIR/snapshots/console-failed-read/tenant-serial-console/POD-STATE.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a healthy capture leaves no Pod-state file" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-healthy
|
|
|
|
run_capture
|
|
|
|
[ ! -f "$COZY_REPORT_DIR/snapshots/console-healthy/tenant-serial-console/POD-STATE.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the Pod-state reads are bounded and taken once for the whole selector" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="p1 p2 p3"
|
|
kubectl_logs_output=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-once
|
|
|
|
run_capture
|
|
|
|
# One describe and one events read for the selector, not per Pod: three
|
|
# silent workers is one finding, and paying for it per Pod would put an
|
|
# unbounded term back into the failure path the cap exists to bound.
|
|
[ "$(grep -c 'describe pods' "$timeout_calls")" -eq 1 ]
|
|
[ "$(grep -c 'get events' "$timeout_calls")" -eq 1 ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the attach check accepts the container wherever KubeVirt puts it" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
# It is an init container today. The question the check asks is whether the
|
|
# container exists at all, so reading both lists costs nothing and removes
|
|
# the one way a KubeVirt-side move would red a green run over a diagnostic.
|
|
kubectl_init_names="virt-launcher-worker-a-11111= guest-console-log"
|
|
accept_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || accept_rc=$?
|
|
[ "$accept_rc" -eq 0 ]
|
|
|
|
kubectl_init_names="virt-launcher-worker-a-11111="
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
|
|
[ "$attach_rc" -eq 2 ]
|
|
assert_file_contains '{.spec.containers[*].name}' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a passing attach check records the positive outcome, not just the negative" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="$(printf 'p1=\np2=guest-console-log')"
|
|
|
|
( set +x; cozy_assert_guest_console_attached ) >"$tmp/out" 2>&1
|
|
|
|
# This run doubles as the revalidation of a platform-wide workaround, so the
|
|
# positive outcome has to leave an artifact too. Returning 0 in silence
|
|
# leaves "the override worked" to be inferred from the absence of a
|
|
# complaint -- the exact inference this collector refuses to make elsewhere.
|
|
assert_file_contains 'attached on 1 of 2 virt-launcher Pods' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a truncated read names the error file when there is one" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_logs_stderr="Warning: stream reset"
|
|
timeout_fail_pod=virt-launcher-worker-a-11111
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-trunc-err
|
|
|
|
run_capture
|
|
|
|
# Same discipline as the silent branch: name a file only when it is there.
|
|
d="$COZY_REPORT_DIR/snapshots/console-trunc-err/tenant-serial-console"
|
|
assert_file_contains 'truncated' "$d/virt-launcher-worker-a-11111.log"
|
|
assert_file_contains 'read-error.log' "$d/virt-launcher-worker-a-11111.log"
|
|
assert_file_contains 'stream reset' "$d/virt-launcher-worker-a-11111.read-error.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a truncated read with no error file does not name one" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
timeout_fail_pod=virt-launcher-worker-a-11111
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-trunc-noerr
|
|
|
|
run_capture
|
|
|
|
d="$COZY_REPORT_DIR/snapshots/console-trunc-noerr/tenant-serial-console"
|
|
assert_file_contains 'truncated' "$d/virt-launcher-worker-a-11111.log"
|
|
assert_file_lacks_pattern 'read-error.log' "$d/virt-launcher-worker-a-11111.log"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the typical-cost figure in both chainsaw comments counts the Pod-state reads" {
|
|
# A comment carrying a number that the code contradicts is worse than no
|
|
# number: it outlives the session and is trusted by the next reader.
|
|
for f in hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml \
|
|
hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml; do
|
|
grep -q '4m05s' "$f" || {
|
|
echo "expected $f to state the minReplicas-2 cost including the wedge and Pod-state reads" >&2
|
|
return 1
|
|
}
|
|
grep -q '6m25s' "$f" || {
|
|
echo "expected $f to state the worst-case cost including the wedge read" >&2
|
|
return 1
|
|
}
|
|
done
|
|
}
|
|
|
|
@test "the attach read gives every matched Pod a non-empty line" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="virt-launcher-worker-a-11111=guest-console-log"
|
|
|
|
( set +x; cozy_assert_guest_console_attached )
|
|
|
|
# Asserted on the query rather than the answer, because no mock can stand in
|
|
# for kubectl's jsonpath engine and this is the property the answer depends
|
|
# on. Without the Pod name, a Pod whose initContainers list is absent emits a
|
|
# bare newline that command substitution strips, so "Pods matched, none
|
|
# carries the container" arrives byte-identical to "no Pod matched" -- and
|
|
# the check silently stops being able to report the one thing it is for.
|
|
assert_file_contains '{.metadata.name}{"="}{.spec.initContainers[*].name}' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the failure-path diagnostics carry a client deadline too" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="virt-launcher-worker-a-11111="
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
|
|
|
|
[ "$attach_rc" -eq 2 ]
|
|
# timeout bounds the process; --request-timeout bounds the call the process
|
|
# makes. The wrapper alone leaves kubectl free to sit in its own retry loop
|
|
# for the whole 30s and return nothing.
|
|
[ "$(grep -c -- '--request-timeout=30s' "$kubectl_calls")" -eq 3 ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "one Pod carrying the container is enough to pass" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="$(printf 'virt-launcher-worker-a-11111=\nvirt-launcher-worker-b-22222=guest-console-log')"
|
|
|
|
attach_rc=0
|
|
( set +x; cozy_assert_guest_console_attached ) || attach_rc=$?
|
|
|
|
# Deliberately "any", not "every": the namespace is not scoped to this
|
|
# release, so an every-Pod rule would fail a run over a Pod that is not
|
|
# this test's. It can miss; it cannot false-fail.
|
|
[ "$attach_rc" -eq 0 ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the attach read is bounded like every other read here" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_init_names="virt-launcher-worker-a-11111=guest-console-log"
|
|
|
|
( set +x; cozy_assert_guest_console_attached )
|
|
|
|
assert_file_contains '-k 5 30 kubectl -n tenant-test get pods' "$timeout_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "only an answered read that lacks the container fails the suite" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# The two non-zero outcomes must not be treated alike: exit 1 says the read
|
|
# did not answer and is not evidence, exit 2 says it did and the container
|
|
# is absent.
|
|
grep -q 'if \[ "${attach_rc}" -eq 2 \]; then' "$lib"
|
|
}
|
|
|
|
@test "the console capture stops at a cap and records what it dropped" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="p1 p2 p3 p4 p5 p6 p7 p8"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-capped
|
|
|
|
run_capture
|
|
|
|
d="$COZY_REPORT_DIR/snapshots/console-capped/tenant-serial-console"
|
|
[ -f "$d/p6.log" ]
|
|
[ ! -f "$d/p7.log" ]
|
|
assert_file_contains 'stopped after 6 Pods; 8 matched in total' "$d/COLLECTION-TRUNCATED.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "an uncapped pool leaves no truncation marker" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="p1 p2"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-uncapped
|
|
|
|
run_capture
|
|
|
|
[ ! -f "$COZY_REPORT_DIR/snapshots/console-uncapped/tenant-serial-console/COLLECTION-TRUNCATED.txt" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the attach check sits on the passing path, after the functional block" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# Two placements are wrong and this pins against both. In the failure block
|
|
# it would answer only in the run where the console can no longer be
|
|
# recovered. Ahead of the functional assertions it would let a diagnostic
|
|
# preempt the checks the suite exists for.
|
|
gate=$(grep -n '^ *cozy_assert_guest_console_attached || attach_rc=$?$' "$lib" | head -n 1 | cut -d: -f1)
|
|
capture=$(grep -n "^ *cozy_capture_tenant_serial_console 'node-join failed" "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$gate" ]; then
|
|
echo "expected the suite to verify guest-console-log attached on the passing path" >&2
|
|
return 1
|
|
fi
|
|
if [ -z "$capture" ]; then
|
|
echo "expected to find the failure-path console capture in $lib" >&2
|
|
return 1
|
|
fi
|
|
if [ "$gate" -le "$capture" ]; then
|
|
echo "the attach check (line $gate) must sit after node-join succeeds, not in the failure block (line $capture)" >&2
|
|
return 1
|
|
fi
|
|
# Anchored on the LAST functional assertion in the function, not a
|
|
# mid-file one, or everything between it and the gate is unprotected.
|
|
#
|
|
# On the call rather than on the text of its arguments. The arguments name
|
|
# local variables of the library, which a refactor there is free to rename
|
|
# without touching what this test asserts; an anchor that spells them out
|
|
# stops matching on that rename, and the failure lands on whoever rebases
|
|
# second rather than on either author. The call is what the ordering is
|
|
# about, so it is what the anchor reads.
|
|
last=$(grep -n '^ *cozy_guard_all_helmreleases ' "$lib" | tail -n 1 | cut -d: -f1)
|
|
if [ -z "$last" ]; then
|
|
echo "expected to find the HelmRelease remediation guard in $lib" >&2
|
|
return 1
|
|
fi
|
|
if [ "$gate" -le "$last" ]; then
|
|
echo "the attach check (line $gate) must sit after every functional assertion (last at line $last)" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the node-join failure path captures the console before the talosctl capture" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# Ordering is the whole point: the talosctl capture needs an apid that the
|
|
# failure class this collects never reached, so running it first spends the
|
|
# failure path's budget on the collector that cannot answer.
|
|
console=$(grep -n "^ *cozy_capture_tenant_serial_console 'node-join failed" "$lib" | head -n 1 | cut -d: -f1)
|
|
talos=$(grep -n '^ *cozy_capture_tenant_talos "${test_name}" || true$' "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$console" ]; then
|
|
echo "expected the node-join failure path to capture the guest serial console" >&2
|
|
return 1
|
|
fi
|
|
if [ -z "$talos" ]; then
|
|
echo "expected to find the talosctl capture call in $lib" >&2
|
|
return 1
|
|
fi
|
|
if [ "$console" -ge "$talos" ]; then
|
|
echo "serial console capture (line $console) must run before the talosctl capture (line $talos)" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the passing path captures the console and leaves teardown to Chainsaw finally" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# An instrument that only runs on the failure path answers in one direction.
|
|
# Every console this tree has ever captured came from a run that failed, so
|
|
# the boot stages a healthy worker goes through, and how long each takes, have
|
|
# no measured value to compare a failing run against -- and "slower than
|
|
# usual" is the claim the failure keeps resting on. The passing run is where
|
|
# that value comes from, and it exists only until the tenant is deleted.
|
|
gate=$(grep -n '^ *cozy_assert_guest_console_attached || attach_rc=$?$' "$lib" | head -n 1 | cut -d: -f1)
|
|
green=$(grep -n "^ *if ! cozy_capture_tenant_serial_console 'the suite passed" "$lib" | head -n 1 | cut -d: -f1)
|
|
function_end=$(awk -v start="$green" 'NR > start && /^}$/ { print NR; exit }' "$lib")
|
|
for v in gate green function_end; do
|
|
eval "n=\$$v"
|
|
if [ -z "$n" ]; then
|
|
echo "expected to locate $v in $lib" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
# After the attach check, because that check is what says there is a container
|
|
# to read. The function must not start an asynchronous tenant teardown after
|
|
# the capture: each suite's finally block owns the single authoritative delete.
|
|
if [ "$green" -le "$gate" ]; then
|
|
echo "the passing-path capture (line $green) must sit after the attach check ($gate)" >&2
|
|
return 1
|
|
fi
|
|
if sed -n "${green},${function_end}p" "$lib" | grep -Eq 'delete kubernetes(nodeses|es)[.]apps[.]cozystack[.]io'; then
|
|
echo "run_kubernetes_test still starts tenant teardown on its success path" >&2
|
|
return 1
|
|
fi
|
|
for suite in latest previous; do
|
|
grep -Fq "cozy_cleanup test-${suite}-version" "hack/e2e-chainsaw/kubernetes-${suite}/chainsaw-test.yaml" || {
|
|
echo "kubernetes-${suite} finally does not own authoritative tenant teardown" >&2
|
|
return 1
|
|
}
|
|
done
|
|
}
|
|
|
|
@test "the passing path asks for the pool minimum, not the pool" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# The failure path runs inside a phase whose budget is derived against the
|
|
# whole operation, so it can afford the pool at its maximum. The passing path
|
|
# has neither a phase nor a budget: it runs inside the same 67m operation the
|
|
# suite has already spent most of, and every read it issues is bounded
|
|
# individually but not as a group. A walk over the maximum pool could
|
|
# therefore turn a suite that proved everything it exists to prove into a red
|
|
# run for collecting a debugging aid slowly. Two workers booting is the same
|
|
# baseline as ten, so that is what it asks for.
|
|
green=$(grep -n "cozy_capture_tenant_serial_console 'the suite passed[^']*' 2;" "$lib" | head -n 1 | cut -d: -f1)
|
|
red=$(grep -n "cozy_capture_tenant_serial_console 'node-join failed[^']*' 6 || true" "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$green" ]; then
|
|
echo "expected the passing path to cap its console walk at the pool minimum" >&2
|
|
return 1
|
|
fi
|
|
if [ -z "$red" ]; then
|
|
echo "expected the failure path to keep covering the pool at its maximum" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the walk covers exactly the number of Pods the caller asked for" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-a virt-launcher-b virt-launcher-c"
|
|
kubectl_logs_output=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-cap-arg
|
|
|
|
out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 2 ) 2>/dev/null )
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
|
|
# The cap is the caller's, not a literal in the walk, or the passing path
|
|
# cannot be cheaper than the failure path and the argument above is decorative.
|
|
dir="$COZY_REPORT_DIR/snapshots/console-cap-arg/tenant-serial-console"
|
|
[ -f "$dir/virt-launcher-a.log" ]
|
|
[ -f "$dir/virt-launcher-b.log" ]
|
|
[ ! -f "$dir/virt-launcher-c.log" ]
|
|
assert_file_contains 'capture stopped after 2 Pods; 3 matched in total' \
|
|
"$dir/COLLECTION-TRUNCATED.txt"
|
|
# And the count on stdout is over the Pods actually read. The cap probe runs
|
|
# on the Pod past the cap, so the counter driving it is one higher than the
|
|
# walk; reported as the denominator it would name a number that is neither
|
|
# what was read nor what matched, and disagree with the truncation marker
|
|
# written beside it -- on the path where nobody opens that marker.
|
|
assert_file_contains 'of the 2 guest consoles read, 2 came back empty and 0 were cut short' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a passing-path capture that failed is said out loud instead of failing the suite" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# Two failure modes, and this pins against both. Left bare, a non-zero return
|
|
# would fail a suite that proved everything it exists to prove, on a
|
|
# debugging aid. Silenced with `|| true`, a capture that collected nothing
|
|
# would leave a green run whose only trace of the gap is an artifact nobody
|
|
# opens on a green run.
|
|
green=$(grep -n "^ *if ! cozy_capture_tenant_serial_console 'the suite passed" "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$green" ]; then
|
|
echo "expected the passing path to capture the console through a tested call" >&2
|
|
return 1
|
|
fi
|
|
# Bounded by the branch rather than by a line count: the warning is what the
|
|
# branch is for, and a fixed offset would break on a comment being added to it
|
|
# while a whole-file search would accept a warning belonging to something
|
|
# else.
|
|
warn=$(awk -v g="$green" '
|
|
NR <= g { next }
|
|
/^ fi$/ { exit }
|
|
/WARNING: the guest console capture/ { print NR; exit }
|
|
' "$lib")
|
|
if [ -z "$warn" ]; then
|
|
echo "expected the failed passing-path capture to warn in the job log" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the capture says which run produced it" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-context
|
|
|
|
run_capture 'the suite passed: captured after the last assertion'
|
|
|
|
# Both paths write the same directory, and the two readings mean opposite
|
|
# things: one is the failure being studied, the other is the baseline it is
|
|
# compared against. Which one a tarball holds is otherwise recoverable only
|
|
# from the run's verdict somewhere else entirely.
|
|
assert_file_contains 'the suite passed: captured after the last assertion' \
|
|
"$COZY_REPORT_DIR/snapshots/console-context/tenant-serial-console/CAPTURE-CONTEXT.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a walk that came back with empty consoles says so on stdout" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_logs_output=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-silent-count
|
|
|
|
out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 6 ) 2>/dev/null )
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
|
|
# The per-pod notes and POD-STATE.txt already record this in the report, and
|
|
# on the failure path that is enough because the report gets opened. The
|
|
# passing path is the one that does not: a baseline that came back empty
|
|
# there would otherwise be visible only to whoever downloads a green run's
|
|
# artifact. The count is not a failure and the capture still returns zero, so
|
|
# stdout is the only place the two callers share.
|
|
assert_file_contains 'of the 1 guest consoles read, 1 came back empty and 0 were cut short' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a healthy walk says nothing about silent consoles" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-silent-none
|
|
|
|
out=$( ( set +x; cozy_capture_tenant_serial_console 'a unit test' 6 ) 2>/dev/null )
|
|
printf '%s\n' "$out" >"$tmp/out"
|
|
|
|
# The other half of the pair. A line printed on every run is a line nobody
|
|
# reads, and this one is meant to be read on the green path.
|
|
assert_file_lacks_pattern 'guest consoles read, ' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the context is written even when there is nothing to capture" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-context-empty
|
|
rc=0
|
|
|
|
run_capture 'the suite passed: captured after the last assertion' || rc=$?
|
|
|
|
# The empty case is the one where the context matters most: a directory
|
|
# holding only a failure note is where a reader most needs to know which run
|
|
# left it, and the early return is the path most likely to skip writing it.
|
|
[ "$rc" -ne 0 ]
|
|
assert_file_contains 'the suite passed' \
|
|
"$COZY_REPORT_DIR/snapshots/console-context-empty/tenant-serial-console/CAPTURE-CONTEXT.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "each console says when it was read, so its last kernel stamp has a wall clock to sit against" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-read-instant
|
|
|
|
run_capture
|
|
|
|
# A console log is stamped in guest seconds since the guest's own kernel
|
|
# started, and it ends wherever the guest had got to when the read ran. Those
|
|
# two facts alone cannot say whether a short log means a guest that started
|
|
# late or a guest that went quiet, because the instant of the read is not in
|
|
# the file. Without this line the only trace of it is the artifact's mtime,
|
|
# which is metadata rather than content: nothing in the report says it, and a
|
|
# reader has to know to go looking outside the file for it.
|
|
f="$COZY_REPORT_DIR/snapshots/console-read-instant/tenant-serial-console/virt-launcher-worker-a-11111.log"
|
|
assert_file_contains 'console read started at ' "$f"
|
|
assert_file_contains ' epoch seconds' "$f"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the capture records when each guest started, beside the console it captured" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline
|
|
|
|
run_capture
|
|
|
|
# The other end of the subtraction. With the read instant above and the
|
|
# compute container's start here, how long the guest had been alive when the
|
|
# console was read is one subtraction away, and comparing that against the
|
|
# last stamp in the log says which of the two happened: a guest that had not
|
|
# lived long enough to reach a milestone, or one that lived and stopped
|
|
# printing. Without it both look like the same short file.
|
|
f="$COZY_REPORT_DIR/snapshots/console-timeline/tenant-serial-console/CAPTURE-TIMELINE.txt"
|
|
assert_file_contains 'computeStartedAt=2026-08-15T14:35:47Z' "$f"
|
|
# The subtraction is spelled out rather than left to be reconstructed. This
|
|
# file is read by whoever opens a failed run's tarball months later, and the
|
|
# relation between a guest-relative kernel stamp and two absolute instants is
|
|
# exactly the step a reader gets wrong.
|
|
assert_file_contains 'epoch seconds' "$f"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a start-time read that failed is named rather than left out" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
# Non-zero AND nothing written: the read that never got a row across, which is
|
|
# a different fact from the one cut off part way through and takes a different
|
|
# note. Both have to be staged explicitly or the mock decides which arm runs.
|
|
kubectl_timeline_rc=9
|
|
kubectl_timeline_rows=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-failed
|
|
|
|
run_capture
|
|
|
|
# An absent file reads as a capture that had no reason to write one, which is
|
|
# the silence every collector here is built to refuse. The read failing is a
|
|
# statement about this machine's view of the cluster, not about when the
|
|
# guests started, and the file has to say which of those it is holding.
|
|
f="$COZY_REPORT_DIR/snapshots/console-timeline-failed/tenant-serial-console/CAPTURE-TIMELINE.txt"
|
|
assert_file_contains 'exit 9' "$f"
|
|
assert_file_lacks_pattern 'computeStartedAt=2026' "$f"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a start-time read cut off part way is not reported as having observed nothing" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_timeline_rc=124
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-cut
|
|
|
|
run_capture
|
|
|
|
# A read killed at its bound can leave rows behind and still report failure.
|
|
# Saying "when these guests started was not observed" under start times that
|
|
# are plainly sitting there makes the file contradict itself, and a reader who
|
|
# believes the note discards the rows -- which for a capture that exists to be
|
|
# subtracted is the whole value of the file.
|
|
f="$COZY_REPORT_DIR/snapshots/console-timeline-cut/tenant-serial-console/CAPTURE-TIMELINE.txt"
|
|
assert_file_contains 'computeStartedAt=2026-08-15T14:35:47Z' "$f"
|
|
assert_file_contains 'cut off (exit 124)' "$f"
|
|
assert_file_lacks_pattern 'was not observed, which is not the same' "$f"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a start-time read that answered with nothing is not reported as a start time" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_timeline_rows=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-empty
|
|
|
|
run_capture
|
|
|
|
# The third outcome, and the one that looks most like success: the read
|
|
# answered, and answered with nothing. A zero-length file here would be read
|
|
# as a capture that ran and found the guests had no start times, which is not
|
|
# a state a Pod can be in -- the walk above just read consoles from these
|
|
# same Pods.
|
|
f="$COZY_REPORT_DIR/snapshots/console-timeline-empty/tenant-serial-console/CAPTURE-TIMELINE.txt"
|
|
assert_file_contains 'returned no rows' "$f"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the start-time read is bounded like every other read in this capture" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-bound
|
|
|
|
run_capture
|
|
|
|
# This read runs on the same failure path as the rest, where the apiserver is
|
|
# the component least likely to answer. An unbounded one does not lose only
|
|
# itself: it holds the op until the op is killed, and the tenant snapshot
|
|
# queued behind it is lost rather than truncated.
|
|
assert_file_contains 'computeStartedAt' "$timeout_calls"
|
|
assert_file_contains 'request-timeout' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a warning beside a healthy start-time read is not filed as an error" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_timeline_stderr='Warning: v1 Pod is deprecated in this build'
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-warn
|
|
|
|
run_capture
|
|
|
|
# kubectl writes deprecation and partial-result warnings on stderr with a zero
|
|
# exit, so what a message means is decided by the status and not by the fact
|
|
# that something was written. Filed as an error, a healthy read sends the next
|
|
# reader after a failure that did not happen; folded into the rows, it sits
|
|
# inside the data the success probe reads.
|
|
dir="$COZY_REPORT_DIR/snapshots/console-timeline-warn/tenant-serial-console"
|
|
assert_file_contains 'v1 Pod is deprecated' "$dir/CAPTURE-TIMELINE-warnings.txt"
|
|
[ ! -f "$dir/CAPTURE-TIMELINE-error.log" ]
|
|
assert_file_lacks_pattern 'deprecated' "$dir/CAPTURE-TIMELINE.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the stderr of a start-time read that failed is kept as the diagnosis" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_timeline_rc=1
|
|
kubectl_timeline_rows=
|
|
kubectl_timeline_stderr='error: the server could not find the requested resource'
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-err
|
|
|
|
run_capture
|
|
|
|
# The other direction: on a failed read that message IS the diagnosis, and
|
|
# dropping it leaves a note saying the read failed with nothing saying why.
|
|
# The note has to name the file, or the evidence sits in a directory the
|
|
# reader has no reason to open.
|
|
dir="$COZY_REPORT_DIR/snapshots/console-timeline-err/tenant-serial-console"
|
|
assert_file_contains 'could not find the requested resource' "$dir/CAPTURE-TIMELINE-error.log"
|
|
[ ! -f "$dir/CAPTURE-TIMELINE-warnings.txt" ]
|
|
assert_file_contains 'CAPTURE-TIMELINE-error.log' "$dir/CAPTURE-TIMELINE.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a start-time read that failed without a word does not point at a missing file" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
kubectl_timeline_rc=124
|
|
kubectl_timeline_rows=
|
|
kubectl_timeline_stderr=
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-silent
|
|
|
|
run_capture
|
|
|
|
# timeout kills the child without a word, so this read can fail having written
|
|
# to neither stream. Naming the error log unconditionally would then send the
|
|
# reader after a file that was never created, which is the same trap the
|
|
# per-Pod notes above are written to avoid.
|
|
dir="$COZY_REPORT_DIR/snapshots/console-timeline-silent/tenant-serial-console"
|
|
[ ! -f "$dir/CAPTURE-TIMELINE-error.log" ]
|
|
assert_file_contains 'gave no reason on either stream' "$dir/CAPTURE-TIMELINE.txt"
|
|
assert_file_lacks_pattern 'reason it gave is in' "$dir/CAPTURE-TIMELINE.txt"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the console container's start is asked for in both status lists" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
kubectl_calls="$tmp/kubectl.calls"
|
|
timeout_calls="$tmp/timeout.calls"
|
|
kubectl_pod_names="virt-launcher-worker-a-11111"
|
|
COZY_REPORT_DIR="$tmp/report"
|
|
COZY_SNAPSHOT_NAME=console-timeline-lists
|
|
|
|
run_capture
|
|
|
|
# KubeVirt runs guest-console-log as an init container, so its runtime state
|
|
# lives under initContainerStatuses. Asking only containerStatuses returns
|
|
# empty for a container that started perfectly well, and an empty start time
|
|
# here is supposed to mean the container never ran -- the one reading this
|
|
# field exists to support. The attach check reads both spec lists for the
|
|
# same reason; a missing key costs an empty string, so naming both is free.
|
|
assert_file_contains 'initContainerStatuses[?(@.name=="guest-console-log")]' "$kubectl_calls"
|
|
assert_file_contains 'containerStatuses[?(@.name=="compute")]' "$kubectl_calls"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a run near its operation ceiling declines the baseline instead of being killed collecting it" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
# The capture costs real wall clock and runs after everything the suite
|
|
# proves. Inside an operation with a fixed ceiling that makes it a way for a
|
|
# green run to end red, which is the one thing a passing-path collector may
|
|
# not do. Driven rather than grepped: the direction of the comparison IS the
|
|
# safety property, and an inverted one runs the capture on exactly the runs
|
|
# that cannot afford it while every structural check still passes.
|
|
date_now=1000000000
|
|
_COZY_RUN_STARTED_AT=$(( date_now - (COZY_OP_CEILING - COZY_GREEN_CAPTURE_RESERVE) - 1 ))
|
|
rc=0
|
|
( set +x; cozy_green_capture_has_room 'the baseline' ) >"$tmp/out" 2>&1 || rc=$?
|
|
|
|
[ "$rc" -eq 1 ]
|
|
# And the decline is spoken. A collector that stops running silently leaves a
|
|
# green run that looks exactly like one where the baseline was collected and
|
|
# came back empty, which is the reading every note in this file exists to
|
|
# prevent.
|
|
assert_file_contains 'not collected' "$tmp/out"
|
|
assert_file_contains 'the baseline' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a run with the operation's room to spare collects the baseline" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
# The other direction, and the one an inverted comparison breaks silently: a
|
|
# gate that declines a run with forty minutes left costs the baseline on every
|
|
# healthy run, and the only symptom is a diagnostic that stopped appearing.
|
|
date_now=1000000000
|
|
_COZY_RUN_STARTED_AT=$(( date_now - 60 ))
|
|
rc=0
|
|
( set +x; cozy_green_capture_has_room 'the baseline' ) >"$tmp/out" 2>&1 || rc=$?
|
|
|
|
[ "$rc" -eq 0 ]
|
|
assert_file_lacks_pattern 'not collected' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a run whose start was never stamped declines rather than dying on the arithmetic" {
|
|
. hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
tmp=$(mktemp -d)
|
|
# The stamp is set by run_kubernetes_test, which no unit test invokes, so
|
|
# nothing else here would notice it being deleted. Under the `set -eu` the
|
|
# chainsaw script runs with, an unbound name inside the arithmetic aborts the
|
|
# whole function -- ending a passing run red, which is the outcome this gate
|
|
# exists to prevent. Declared zero at file scope, it fails closed instead.
|
|
date_now=1000000000
|
|
rc=0
|
|
( set -u; set +x; cozy_green_capture_has_room 'the baseline' ) >"$tmp/out" 2>&1 || rc=$?
|
|
|
|
[ "$rc" -eq 1 ]
|
|
assert_file_contains 'not collected' "$tmp/out"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "the passing path decides through the gate rather than around it" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# The behavioural tests above cover the predicate. Its wiring is what they
|
|
# cannot reach: run_kubernetes_test is not callable from a unit test, so the
|
|
# stamp it takes and the gate it calls are only assertable as source. Both
|
|
# halves fail the same silent way -- a correct predicate nothing calls
|
|
# protects nothing, and a predicate called without the stamp reads the whole
|
|
# epoch as elapsed and declines the baseline on every run forever.
|
|
stamp=$(grep -n '^ _COZY_RUN_STARTED_AT=\$(date +%s)$' "$lib" | head -n 1 | cut -d: -f1)
|
|
gate=$(grep -n 'cozy_green_capture_has_room ' "$lib" | tail -n 1 | cut -d: -f1)
|
|
green=$(grep -n "cozy_capture_tenant_serial_console 'the suite passed" "$lib" | head -n 1 | cut -d: -f1)
|
|
for v in stamp gate green; do
|
|
eval "n=\$$v"
|
|
if [ -z "$n" ]; then
|
|
echo "expected to locate $v in $lib" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
# Inside the function whose elapsed time it measures, not at file scope: taken
|
|
# once when the library is sourced, it would time the sourcing rather than the
|
|
# run, and every suite after the first would read as already over its ceiling.
|
|
fn=$(grep -n '^run_kubernetes_test() {$' "$lib" | head -n 1 | cut -d: -f1)
|
|
if [ -z "$fn" ] || [ "$stamp" -le "$fn" ]; then
|
|
echo "the run's start stamp (line $stamp) must be taken inside run_kubernetes_test (line $fn)" >&2
|
|
return 1
|
|
fi
|
|
if [ "$stamp" -ge "$gate" ] || [ "$gate" -ge "$green" ]; then
|
|
echo "expected the stamp ($stamp) before the gate ($gate) before the capture ($green)" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the operation ceiling the reserve is measured against is the one the suites give" {
|
|
lib=hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
# A reserve is only meaningful against the right ceiling. This number is a
|
|
# restatement of one that lives in the suite files, so it is read from both
|
|
# sides rather than trusted: a suite that raises or lowers its `timeout:`
|
|
# otherwise leaves the reserve protecting a ceiling that moved, and the
|
|
# decline either stops firing when it should or starts firing when it should
|
|
# not.
|
|
ceiling=$(grep -oE '^COZY_OP_CEILING=[0-9]+' "$lib" | head -n 1 | sed -E 's/.*=//')
|
|
reserve=$(grep -oE '^COZY_GREEN_CAPTURE_RESERVE=[0-9]+' "$lib" | head -n 1 | sed -E 's/.*=//')
|
|
for v in ceiling reserve; do
|
|
eval "n=\$$v"
|
|
if [ -z "$n" ]; then
|
|
echo "expected to read $v from $lib; without it this guard reports success for having lost its input" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
for f in hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml \
|
|
hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml; do
|
|
# The first `timeout:` in each file is the bringup op's, which is the one
|
|
# this whole helper runs inside; the teardown step below it has its own.
|
|
minutes=$(grep -oE '^ *timeout: [0-9]+m$' "$f" | head -n 1 | grep -oE '[0-9]+')
|
|
if [ -z "$minutes" ]; then
|
|
echo "expected to read the operation timeout from $f" >&2
|
|
return 1
|
|
fi
|
|
if [ "$((minutes * 60))" -ne "$ceiling" ]; then
|
|
echo "$f gives the operation ${minutes}m but the reserve is measured against ${ceiling}s" >&2
|
|
return 1
|
|
fi
|
|
done
|
|
# And the reserve has to leave room for the capture it is reserving for, or it
|
|
# protects nothing: the walk it admits would still run past the ceiling.
|
|
if [ "$reserve" -lt 210 ]; then
|
|
echo "the reserve ${reserve}s is under the 210s the capture can cost at this caller's cap" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
@test "the e2e tenant node group asks KubeVirt for the guest console log" {
|
|
# Anchored to a bare two-space-indented key so a comment or a prose mention
|
|
# of the field cannot stand in for the setting itself. Since the Phase 2 split
|
|
# the worker pool is a separate KubernetesNodes CR whose fields sit two spaces
|
|
# under spec, not six spaces under a nodeGroups.<name> block. It does not prove
|
|
# the key sits in that CR specifically -- the render is what would catch that,
|
|
# and the chart's own suites cover it.
|
|
grep -q '^ logSerialConsole: true$' hack/e2e-chainsaw/_lib/run-kubernetes.sh
|
|
}
|