Files
cozystack/hack/chainsaw-op-budgets.bats
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
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>
2026-10-07 11:04:00 +03:00

354 lines
21 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Contract: a Chainsaw script op whose script carries its own waits gives those
# waits room to fire first. When the op ceiling fires instead, Chainsaw kills
# the shell, the script's own failure message never prints, and a cleanup trap
# never runs (.chainsaw.yaml states the rule above its catch scripts).
#
# The gateway step is held by arithmetic: its polls are `timeout N` prefixes
# and its cleanup deletes run under `timeout -k K N`, so their sum, kill
# grace included, is readable from the text. kubectl's own `--timeout` is not
# counted: it bounds only the wait after the delete request, not discovery or
# the request itself. Only those two spellings are summed: a wait written any
# other way, a `kubectl wait` or a sleep loop, is invisible to the sum. The
# redis-tls step cannot be summed that way, because some of its
# reads sit in loops and one loop polls, so it carries a step budget that
# every kubectl call is clamped to, and the guard holds that budget under the
# op. Its failure messages are checked by running the step's script against a
# stubbed kubectl.
#
# CI runs this file under Bats. It stays compatible with the legacy cozytest.sh
# translator, whose awk parser recognizes only @test blocks and a bare `}` on its
# own line; a `}` at column zero gets `return 0` inserted ahead of it, so the
# helpers below report through stdout only.
#
# Run with: bats hack/chainsaw-op-budgets.bats
# -----------------------------------------------------------------------------
load test_helper
GATEWAY_SUITE="hack/e2e-chainsaw/gateway/chainsaw-test.yaml"
GATEWAY_STEP="parent-child-and-route-drive-listener-set"
REDIS_SUITE="hack/e2e-chainsaw/redis/chainsaw-test.yaml"
REDIS_STEP="verify-tenant-ca-projection"
COMPUTEPLANE_SUITE="hack/e2e-chainsaw/computeplane/chainsaw-test.yaml"
# Seconds the gateway op keeps above its summed waits for what carries no cap
# of its own: three applies and one label read.
GATEWAY_UNCAPPED=60
# Seconds the redis op keeps above its step budget: the kill grace on the last
# clamped read, one 5s poll sleep, and the openssl and shell work between reads.
REDIS_AFTER_BUDGET=30
# Print the lines of the first `- script:` op inside step $2 of file $1, with
# the op's own indentation, from `- script:` up to the next op or step.
op_lines() {
awk -v step="$2" '
$0 ~ "^[[:space:]]*- name: " step "[[:space:]]*$" { instep = 1; next }
instep && !inop && /^[[:space:]]*- script:[[:space:]]*$/ {
match($0, /^[[:space:]]*/); opindent = RLENGTH; inop = 1; print; next
}
inop && /[^[:space:]]/ {
match($0, /^[[:space:]]*/)
if (RLENGTH <= opindent) exit
}
inop { print }
instep && /^---/ { exit }
' "$1"
}
# Print the op timeout in seconds of the op in $1 (op_lines output), or
# nothing when it states none in minutes or seconds.
op_timeout_seconds() {
printf '%s\n' "$1" | awk '
/^[[:space:]]*timeout:[[:space:]]*[0-9]+m[[:space:]]*$/ { match($0, /[0-9]+/); print substr($0, RSTART, RLENGTH) * 60; exit }
/^[[:space:]]*timeout:[[:space:]]*[0-9]+s[[:space:]]*$/ { match($0, /[0-9]+/); print substr($0, RSTART, RLENGTH); exit }
/^[[:space:]]*content:/ { exit }
'
}
# Print the op's shell, or `sh`, the Chainsaw default, when it names none.
op_shell() {
sh_name=$(printf '%s\n' "$1" | awk '
/^[[:space:]]*shell:[[:space:]]*[^[:space:]]+[[:space:]]*$/ { sub(/^[[:space:]]*shell:[[:space:]]*/, ""); sub(/[[:space:]]*$/, ""); print; exit }
/^[[:space:]]*content:/ { exit }
')
printf '%s\n' "${sh_name:-sh}"
}
# Print the op's script, dedented, without the content key.
op_content() {
printf '%s\n' "$1" | awk '
!incontent && /^[[:space:]]*content:[[:space:]]*\|[[:space:]]*$/ { match($0, /^[[:space:]]*/); keyindent = RLENGTH; incontent = 1; next }
incontent && /[^[:space:]]/ {
match($0, /^[[:space:]]*/)
if (RLENGTH <= keyindent) exit
if (!bodyindent) bodyindent = RLENGTH
}
incontent { print substr($0, bodyindent + 1) }
'
}
# Write a kubectl stub into directory $1 that answers the redis-tls step's
# reads as a healthy cluster would. STUB_FAIL, when set, is a pattern matched
# against the arguments: a matching call prints STUB_ERR to stderr and exits
# STUB_RC instead, after printing STUB_OUT to stdout when that is set, from the
# STUB_FAIL_FROM-th matching call on (default the first). STUB_PROJ_KEY=1 puts
# PEM private key material into the projection. STUB_SLEEP_ON is a pattern
# whose matching calls sleep STUB_SLEEP seconds first; the stub execs sleep,
# so a timeout that kills it leaves no child holding the output pipe.
write_redis_stubs() {
cat > "$1/kubectl" <<'STUB'
#!/bin/sh
args="$*"
if [ -n "${STUB_SLEEP_ON:-}" ]; then
case "$args" in
$STUB_SLEEP_ON) exec sleep "${STUB_SLEEP:-60}" ;;
esac
fi
if [ -n "${STUB_FAIL:-}" ]; then
case "$args" in
$STUB_FAIL)
echo x >> "$(dirname "$0")/fail-matches"
if [ "$(wc -l < "$(dirname "$0")/fail-matches")" -ge "${STUB_FAIL_FROM:-1}" ]; then
[ -z "${STUB_OUT:-}" ] || echo "$STUB_OUT"
echo "${STUB_ERR:-error: stubbed failure}" >&2; exit "${STUB_RC:-1}"
fi ;;
esac
fi
cert_b64=$(printf 'CERT' | base64)
proj_b64=$cert_b64
if [ "${STUB_PROJ_KEY:-}" = 1 ]; then
proj_b64=$(printf 'CERT\n-----BEGIN PRIVATE KEY-----\nKEY\n' | base64 | tr -d '\n')
fi
case "$args" in
*"get secret redis-secure.tenant-ca -o go-template"*) printf 'ca.crt\n' ;;
*"get secret redis-secure.ca-cert -o go-template"*) printf 'ca.crt\n' ;;
*"get secret redis-secure.ca-tls -o go-template"*) printf 'ca.crt\ntls.crt\ntls.key\n' ;;
*"get secret redis-secure.ca-tls -o jsonpath"*) printf '%s' "$cert_b64" ;;
*"get secret redis-secure.tenant-ca -o jsonpath"*) printf '%s' "$proj_b64" ;;
*"get tenantsecret redis-secure.tenant-ca -o jsonpath"*) printf '%s' "$(printf -- '-----BEGIN CERTIFICATE-----' | base64)" ;;
*"get tenantsecret "*) echo "Error from server (NotFound): tenantsecrets \"x\" not found" >&2; exit 1 ;;
*"exec rfr-redis-secure-0 -c redis -- cat /tls/ca.crt"*) printf 'CERT\n' ;;
*"exec rfr-redis-secure-0 -c redis -- timeout 15 redis-cli --tls"*) printf 'PONG\n' ;;
*"exec rfr-redis-secure-0 -c redis -- timeout 15 redis-cli -h"*) printf 'I/O error\n'; exit 1 ;;
*"exec rfr-redis-secure-0 -c redis -- sh -c"*) printf 'role:master\n' ;;
*"exec rfr-redis-secure-1 -c redis -- sh -c"*) printf 'role:slave\nmaster_link_status:up\n' ;;
*"get pod -l app.kubernetes.io/component=sentinel"*) printf 'rfs-redis-secure-0' ;;
*"exec rfs-redis-secure-0 -c sentinel"*) printf '10.0.0.1\n6379\n' ;;
*) echo "kubectl stub: unexpected call: $args" >&2; exit 99 ;;
esac
STUB
cat > "$1/openssl" <<'STUB'
#!/bin/sh
cat >/dev/null
echo "sha256 Fingerprint=AA"
STUB
chmod +x "$1/kubectl" "$1/openssl"
}
# Run the redis-tls step's script, as its op's shell would, against the stubs
# in $1. Prints the script's combined output and then `rc=<status>`. RS_BUDGET,
# when set, replaces the step's budget so the clamp can be driven in seconds.
run_redis_step() {
rs_op=$(op_lines "$REDIS_SUITE" "$REDIS_STEP")
rs_shell=$(op_shell "$rs_op")
op_content "$rs_op" > "$1/step.sh"
if [ -n "${RS_BUDGET:-}" ]; then
sed "s/^step_budget=[0-9]*\$/step_budget=${RS_BUDGET}/" "$1/step.sh" > "$1/step.budget"
if cmp -s "$1/step.sh" "$1/step.budget"; then echo "no step_budget line to override"; echo "rc=97"; return; fi
mv "$1/step.budget" "$1/step.sh"
fi
rs_rc=0
rs_out=$(cd "$1" && PATH="$1:$PATH" NAMESPACE=tenant-test STUB_FAIL="${STUB_FAIL:-}" STUB_FAIL_FROM="${STUB_FAIL_FROM:-1}" STUB_RC="${STUB_RC:-1}" STUB_ERR="${STUB_ERR:-}" STUB_OUT="${STUB_OUT:-}" STUB_SLEEP_ON="${STUB_SLEEP_ON:-}" STUB_SLEEP="${STUB_SLEEP:-}" STUB_PROJ_KEY="${STUB_PROJ_KEY:-}" "$rs_shell" "$1/step.sh" 2>&1) || rs_rc=$?
printf '%s\nrc=%s\n' "$rs_out" "$rs_rc"
}
@test "the gateway listener-set op outlasts the waits inside it" {
gw_op=$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP")
[ -n "$gw_op" ] || { echo "no script op found in step $GATEWAY_STEP of $GATEWAY_SUITE" >&2; exit 1; }
gw_timeout=$(op_timeout_seconds "$gw_op")
case "$gw_timeout" in ''|*[!0-9]*) echo "the $GATEWAY_STEP op states no timeout in m or s (got '${gw_timeout}')" >&2; exit 1 ;; esac
gw_body=$(op_content "$gw_op")
gw_polls=$(printf '%s\n' "$gw_body" | grep -oE '^[[:space:]]*timeout +[0-9]+' | grep -oE '[0-9]+$' || true)
gw_n=$(printf '%s\n' "$gw_polls" | grep -c . || true)
[ "$gw_n" -ge 5 ] || { echo "found ${gw_n} capped polls in $GATEWAY_STEP, expected at least 5; the extractor is reading the wrong lines" >&2; exit 1; }
gw_deletes=$(printf '%s\n' "$gw_body" | grep -E '(^|[[:space:]])kubectl .* delete ' | grep -oE '(^|[[:space:]])timeout -k [0-9]+ [0-9]+ kubectl ' | awk '{ print $3 + $4 }' || true)
gw_sum=0
for gw_w in $gw_polls $gw_deletes; do gw_sum=$(( gw_sum + gw_w )); done
[ $(( gw_timeout - gw_sum )) -ge "$GATEWAY_UNCAPPED" ] || {
echo "the $GATEWAY_STEP op allows ${gw_timeout}s; its polls ($(printf '%s ' $gw_polls)) and cleanup deletes ($(printf '%s ' $gw_deletes)) may wait ${gw_sum}s" >&2
echo "it must keep ${GATEWAY_UNCAPPED}s above them for the uncapped applies and reads" >&2
exit 1
}
}
@test "every delete in the gateway listener-set cleanup is bounded as a whole" {
# A tenant delete blocks until its child namespaces drain, and a stalled
# API request blocks with no bound at all, so a delete not wrapped in an
# outer timeout is a wait the op sum above cannot see.
gw_op=$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP")
gw_deletes=$(op_content "$gw_op" | grep -E '(^|[[:space:]])kubectl .* delete ' || true)
[ -n "$gw_deletes" ] || { echo "no kubectl delete found in $GATEWAY_STEP; the cleanup this test reasons about is gone" >&2; exit 1; }
gw_bad=$(printf '%s\n' "$gw_deletes" | grep -vE '(^|[[:space:]])timeout -k [0-9]+ [0-9]+ kubectl ' || true)
[ -z "$gw_bad" ] || { echo "deletes in $GATEWAY_STEP whose wait has no bound:" >&2; printf '%s\n' "$gw_bad" >&2; exit 1; }
}
@test "the gateway listener-set cleanup deletes the parent only once the child is gone" {
# A parent deleted while its child is still uninstalling wedges the
# parent's cleanup, so a child delete that ran into its bound must leave
# the parent alone.
gw_op=$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP")
gw_order=$(op_content "$gw_op" | awk '
/^[[:space:]]*if timeout -k [0-9]+ [0-9]+ kubectl .* delete tenant rchild .*; then[[:space:]]*$/ { guarded = 1; next }
guarded && /[^[:space:]]/ { print (/delete tenant rparent/ ? "ok" : "unguarded"); exit }
/delete tenant rparent/ { print "unguarded"; exit }
')
[ "$gw_order" = ok ] || { echo "the rparent delete in $GATEWAY_STEP does not sit directly under a successful rchild delete (got '${gw_order}')" >&2; exit 1; }
}
@test "a failed tenant delete in the gateway cleanup fails the step with its own error" {
# A tenant left in place is one every later suite inherits, so
# the step must say so and fail rather than pass with the leak.
gw_dir=$(mktemp -d)
op_content "$(op_lines "$GATEWAY_SUITE" "$GATEWAY_STEP")" | awk '/^cleanup\(\) \{/ { on = 1 } on { print } on && /^\}/ { exit }' > "$gw_dir/cleanup.sh"
grep -q 'delete tenant rchild' "$gw_dir/cleanup.sh" || { echo "no cleanup() with the rchild delete found in $GATEWAY_STEP" >&2; exit 1; }
# The stub fails the delete of the tenant named in STUB_FAIL_TENANT with a
# Forbidden error, which is not the bound firing, and logs every delete.
printf '%s\n' '#!/bin/sh' 'echo "$*" >> "$(dirname "$0")/calls"' 'case "$*" in *"delete tenant $STUB_FAIL_TENANT "*) echo "Error from server (Forbidden): stubbed" >&2; exit 3 ;; esac' > "$gw_dir/kubectl"
chmod +x "$gw_dir/kubectl"
for gw_tenant in rchild rparent; do
: > "$gw_dir/calls"
gw_rc=0
gw_out=$(PATH="$gw_dir:$PATH" STUB_FAIL_TENANT="$gw_tenant" sh -c '. "$1/cleanup.sh"; ns=tenant-test; cleanup' sh "$gw_dir" 2>&1) || gw_rc=$?
[ "$gw_rc" -ne 0 ] || { printf '%s\n' "$gw_out" >&2; echo "cleanup exited 0 with the $gw_tenant delete failed" >&2; exit 1; }
case "$gw_out" in *"Forbidden"*"$gw_tenant delete exited 3"*"rparent is left in tenant-test"*) ;; *) printf '%s\n' "$gw_out" >&2; echo "with the $gw_tenant delete failed, the output does not carry kubectl's error, its exit status and the tenant left behind" >&2; exit 1 ;; esac
if [ "$gw_tenant" = rchild ] && grep -q 'delete tenant rparent' "$gw_dir/calls"; then echo "the parent was deleted although the child delete failed" >&2; exit 1; fi
done
rm -rf "$gw_dir"
}
@test "the redis-tls projection op outlasts the step budget its reads are clamped to" {
rt_op=$(op_lines "$REDIS_SUITE" "$REDIS_STEP")
[ -n "$rt_op" ] || { echo "no script op found in step $REDIS_STEP of $REDIS_SUITE" >&2; exit 1; }
rt_timeout=$(op_timeout_seconds "$rt_op")
case "$rt_timeout" in ''|*[!0-9]*) echo "the $REDIS_STEP op states no timeout in m or s (got '${rt_timeout}')" >&2; exit 1 ;; esac
rt_body=$(op_content "$rt_op")
rt_budget=$(printf '%s\n' "$rt_body" | sed -n 's/^step_budget=\([0-9][0-9]*\)$/\1/p')
[ -n "$rt_budget" ] || {
echo "$REDIS_STEP sets no step_budget, so nothing bounds the sum of its reads below the ${rt_timeout}s op" >&2
echo "its kubectl call sites alone come to $(printf '%s\n' "$rt_body" | grep -cE '(^|[^[:alnum:]_])k -n ')" >&2
exit 1
}
[ $(( rt_timeout - rt_budget )) -ge "$REDIS_AFTER_BUDGET" ] || {
echo "the $REDIS_STEP op allows ${rt_timeout}s and the step budget is ${rt_budget}s" >&2
echo "it must keep ${REDIS_AFTER_BUDGET}s above the budget for the last read's kill grace and the work between reads" >&2
exit 1
}
# The budget bounds only the reads that go through k(). A bare kubectl
# outside it would add an uncapped read the arithmetic above never sees.
rt_bare=$(printf '%s\n' "$rt_body" | grep -vE '^[[:space:]]*#' | grep -nE '(^|[^[:alnum:]_-])kubectl([[:space:]]|$)' | grep -v '"\$k_left" kubectl' || true)
[ -z "$rt_bare" ] || { echo "kubectl calls in $REDIS_STEP that bypass the clamped k():" >&2; printf '%s\n' "$rt_bare" >&2; exit 1; }
}
@test "the redis-tls projection script passes against a healthy stub" {
# The positive control for the two failure tests below: without it, a
# stub that broke the script somewhere else would make them pass.
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_out=$(run_redis_step "$rt_dir")
case "$rt_out" in *"
rc=0") ;; *) printf '%s\n' "$rt_out" >&2; echo "the step failed against a healthy stub" >&2; exit 1 ;; esac
rm -rf "$rt_dir"
}
@test "a failed replication exec reports its own error, not the 15s cap" {
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_out=$(STUB_FAIL='*exec rfr-redis-secure-1 -c redis -- sh -c*' STUB_RC=1 \
STUB_ERR='Error from server (Forbidden): pods "rfr-redis-secure-1" is forbidden' run_redis_step "$rt_dir")
case "$rt_out" in *"
rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a forbidden exec" >&2; exit 1 ;; esac
case "$rt_out" in *"15s cap"*) printf '%s\n' "$rt_out" >&2; echo "a forbidden exec was reported as the 15s cap firing" >&2; exit 1 ;; esac
case "$rt_out" in *"rfr-redis-secure-1"*"exit 1"*"Forbidden"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the message does not carry the exit status and what the exec printed" >&2; exit 1 ;; esac
rm -rf "$rt_dir"
}
@test "a failed sentinel exec reports its own error, not the 15s cap" {
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_out=$(STUB_FAIL='*exec rfs-redis-secure-0 -c sentinel*' STUB_RC=1 \
STUB_ERR='command terminated with exit code 1' STUB_OUT='NOAUTH Authentication required.' run_redis_step "$rt_dir")
case "$rt_out" in *"
rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a failed sentinel exec" >&2; exit 1 ;; esac
case "$rt_out" in *"15s cap"*) printf '%s\n' "$rt_out" >&2; echo "a refused sentinel command was reported as the 15s cap firing" >&2; exit 1 ;; esac
case "$rt_out" in *"command terminated with exit code 1"*"rfs-redis-secure-0 failed (exit 1): NOAUTH Authentication required."*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the output does not carry what the exec wrote to both streams and its exit status" >&2; exit 1 ;; esac
rm -rf "$rt_dir"
}
@test "a failed Secret read is not reported as a wrong source name" {
# base64 -d exits 0 on empty input, so without pipefail the pipeline takes
# its status and the emptiness check below names the wrong cause.
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_out=$(STUB_FAIL='*get secret redis-secure.ca-tls -o jsonpath*' STUB_RC=1 \
STUB_ERR='Error from server (ServiceUnavailable): the server is currently unable to handle the request' run_redis_step "$rt_dir")
case "$rt_out" in *"
rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a failed Secret read" >&2; exit 1 ;; esac
case "$rt_out" in *"source name or key is wrong"*) printf '%s\n' "$rt_out" >&2; echo "an API error was reported as a wrong source name" >&2; exit 1 ;; esac
case "$rt_out" in *"ServiceUnavailable"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the API error is missing from the output" >&2; exit 1 ;; esac
rm -rf "$rt_dir"
}
@test "the private-key check fails when the projection it reads carries a key" {
# The projection is read once above this check. A check that reads it
# again passes when that second read fails, because an empty read holds
# no key material: here the first read returns a key and the second one
# is refused.
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_out=$(STUB_PROJ_KEY=1 STUB_FAIL='*get secret redis-secure.tenant-ca -o jsonpath*' STUB_FAIL_FROM=2 STUB_RC=1 \
STUB_ERR='Error from server (ServiceUnavailable): the server is currently unable to handle the request' run_redis_step "$rt_dir")
case "$rt_out" in *"
rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with key material in the projection" >&2; exit 1 ;; esac
case "$rt_out" in *"projection carries PEM private key material"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the step did not fail on the key material" >&2; exit 1 ;; esac
rm -rf "$rt_dir"
}
@test "a read that finds the step budget spent does not run and says so" {
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_out=$(RS_BUDGET=0 run_redis_step "$rt_dir")
case "$rt_out" in *"
rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with its budget spent before the first read" >&2; exit 1 ;; esac
case "$rt_out" in *"step budget is spent, so this read did not run: -n tenant-test get secret redis-secure.tenant-ca"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the first read did not report the spent budget" >&2; exit 1 ;; esac
rm -rf "$rt_dir"
}
@test "a read is clamped to what the step budget has left and names itself" {
# A 2s budget against a read that would hang for 20s: the clamp has to
# end the read at the budget, not at the 30s per-read cap.
rt_dir=$(mktemp -d)
write_redis_stubs "$rt_dir"
rt_start=$(date +%s)
rt_out=$(RS_BUDGET=2 STUB_SLEEP_ON='*get secret redis-secure.tenant-ca -o go-template*' STUB_SLEEP=20 run_redis_step "$rt_dir")
rt_took=$(( $(date +%s) - rt_start ))
case "$rt_out" in *"
rc=0") printf '%s\n' "$rt_out" >&2; echo "the step passed with a hung read" >&2; exit 1 ;; esac
case "$rt_out" in *"ran into its "[12]"s bound: -n tenant-test get secret redis-secure.tenant-ca"*) ;; *) printf '%s\n' "$rt_out" >&2; echo "the hung read did not name itself at the budget's bound" >&2; exit 1 ;; esac
[ "$rt_took" -lt 15 ] || { echo "the step took ${rt_took}s against a 2s budget; the read was not clamped" >&2; exit 1; }
rm -rf "$rt_dir"
}
@test "every kubectl in the computeplane scripts runs under its own timeout" {
# The suite's budget comments sum op ceilings and inner bounds, which hold
# only while no read can outlive them; kubectl's own --timeout does not
# count, it leaves the requests before the wait unbounded. Calls inside
# sourced helpers are the helpers' to bound and are not seen here.
cp_scripts=$(yq '.spec.steps[] | (.try[]?, .catch[]?, .finally[]?) | .script.content // ""' "$COMPUTEPLANE_SUITE")
printf '%s\n' "$cp_scripts" | grep -q 'kubectl ' || { echo "no kubectl found in the scripts of $COMPUTEPLANE_SUITE; the check reads nothing" >&2; exit 1; }
cp_bare=$(printf '%s\n' "$cp_scripts" | awk '/^[[:space:]]*#/ { next }
{ line = " " $0; all = gsub(/[^-_[:alnum:]]kubectl /, "&", line); ok = gsub(/timeout (-k [0-9]+ )?([0-9]+|[$][(][(][^)]*[)][)]) kubectl /, "&", line); if (all != ok) print }')
[ -z "$cp_bare" ] || { printf '%s\n' "$cp_bare" >&2; echo "kubectl without a timeout in the computeplane scripts" >&2; exit 1; }
}