Files
cozystack/hack/talos-image-cache_test.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

412 lines
20 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for the in-sandbox Talos image cache manifest split and the
# importer-reachability contract it relies on.
#
# hack/e2e-talos-image-cache.yaml bundles four documents but they are applied in
# two phases: hack/e2e-install-cozystack.bats applies everything EXCEPT the
# CiliumClusterwideNetworkPolicy (its CRD does not exist before Cozystack is
# installed), and hack/e2e-chainsaw/_lib/talos-image-cache.sh applies ONLY that
# policy later, once Cilium is up. If a future document is added to the manifest
# and silently dropped from the pre-Cilium apply, or the Cilium document leaks
# into it (and errors on the missing CRD), the mirror breaks and e2e falls back
# to the flaky public factory. These tests pin that split.
#
# They also pin the load-bearing reachability invariant: the throwaway probe pod
# is a faithful proxy for a real CDI importer only if it carries the exact label
# and namespace the egress policy selects. If either side drifts, the probe can
# pass while real importers stay blocked (a false positive that makes CI worse).
#
# Finally they cover the decision itself, sourced from
# hack/e2e-chainsaw/_lib/talos-image-cache.sh (sourcing it has no cluster side
# effects — it only sets parameter defaults and defines functions). Two pure
# fragments are exercised directly: the strict 206 gate that decides whether
# tenants are pointed at the mirror at all, and the --overrides JSON builder
# whose silent breakage would drop every probe to the public factory. The
# kubectl orchestration around them is driven here too, against a throwaway
# `kubectl` and `timeout` placed on PATH — the decision of which factory a whole
# suite pulls from is worth pinning without waiting for a cluster.
#
# On PATH, not shell functions, and that is not a style choice: the lookup runs
# `timeout -k 5 40 kubectl …` wherever timeout is installed, and an external
# timeout binary execs a real file.
# A `kubectl() { … }` override is invisible to it, so the stub would never be
# reached, and a suite that stubs that way goes quietly vacuous. A stub on PATH
# is also the only way to assert how the real call was spelled, which matters
# for the flags the three-way answer depends on: the stub answers exit 0 with no
# output for an absence no matter what flags it is handed, while a real kubectl
# does that only with --ignore-not-found, so that flag is pinned by inspecting
# the recorded call rather than by the behaviour around it.
#
# Root cause + fix are from cozystack/cozystack#3254 (@lexfrei); this is the port
# to the Chainsaw layout (hack/e2e-chainsaw/_lib/).
#
# CI runs this file under Bats through `make bats-unit-tests`. It also remains
# compatible with the legacy `hack/cozytest.sh` translator, whose awk parser
# recognizes only @test blocks and a bare `}` on its own line. The tests avoid
# `run`, `$status`, setup(), and teardown() for that compatibility path and use
# direct shell assertions. mikefarah yq's document separators are stripped.
#
# Run with: bats hack/talos-image-cache_test.bats
# -----------------------------------------------------------------------------
load test_helper
@test "manifest documents partition into pre-Cilium apply plus the Cilium policy" {
manifest=hack/e2e-talos-image-cache.yaml
total=$(yq '.kind' "$manifest" | grep -vc '^---$')
excluded=$(yq 'select(.kind != "CiliumClusterwideNetworkPolicy") | .kind' "$manifest" | grep -vc '^---$')
selected=$(yq 'select(.kind == "CiliumClusterwideNetworkPolicy") | .kind' "$manifest" | grep -vc '^---$')
[ "$total" -eq 4 ]
[ "$excluded" -eq 3 ]
[ "$selected" -eq 1 ]
[ $((excluded + selected)) -eq "$total" ]
}
@test "pre-Cilium apply keeps Service, Deployment, ConfigMap and drops the Cilium policy" {
manifest=hack/e2e-talos-image-cache.yaml
kinds=$(yq 'select(.kind != "CiliumClusterwideNetworkPolicy") | .kind' "$manifest" | grep -v '^---$')
for want in Service Deployment ConfigMap; do
printf '%s\n' "$kinds" | grep -qx "$want" || { echo "pre-Cilium apply is missing $want" >&2; exit 1; }
done
if printf '%s\n' "$kinds" | grep -qx CiliumClusterwideNetworkPolicy; then
echo "CiliumClusterwideNetworkPolicy leaked into the pre-Cilium apply" >&2
exit 1
fi
}
@test "point-of-use apply selects exactly the Cilium policy" {
manifest=hack/e2e-talos-image-cache.yaml
kinds=$(yq 'select(.kind == "CiliumClusterwideNetworkPolicy") | .kind' "$manifest" | grep -v '^---$')
[ "$kinds" = "CiliumClusterwideNetworkPolicy" ]
}
@test "range server records transfer outcome, bytes and duration" {
script=$(yq 'select(.kind == "ConfigMap") | .data."serve.py"' hack/e2e-talos-image-cache.yaml)
printf '%s\n' "$script" | python3 -c 'import sys; compile(sys.stdin.read(), "serve.py", "exec")'
printf '%s\n' "$script" | grep -Fq 'transfer-finished status=%s path=%s range=%s requested=%d sent=%d duration=%.3fs'
printf '%s\n' "$script" | grep -Fq 'time.monotonic() - started'
}
@test "egress policy selects the importer label and namespace the probe pod uses" {
manifest=hack/e2e-talos-image-cache.yaml
helper=hack/e2e-chainsaw/_lib/talos-image-cache.sh
label=$(yq 'select(.kind == "CiliumClusterwideNetworkPolicy") | .spec.endpointSelector.matchLabels["k8s:cdi.kubevirt.io"]' "$manifest")
ns=$(yq 'select(.kind == "CiliumClusterwideNetworkPolicy") | .spec.endpointSelector.matchLabels["k8s:io.kubernetes.pod.namespace"]' "$manifest")
[ "$label" = "importer" ]
[ "$ns" = "tenant-test" ]
# The probe pod must carry the same label the policy selects, else it faces a
# different egress than a real importer and the guarantee breaks.
grep -q "cdi.kubevirt.io=importer" "$helper" || { echo "probe pod label drifted from the egress selector" >&2; exit 1; }
grep -q "TALOS_IMAGE_CACHE_PROBE_NS:-tenant-test" "$helper" || { echo "probe namespace drifted from the egress selector" >&2; exit 1; }
}
@test "egress rule targets the mirror pod's own identity" {
manifest=hack/e2e-talos-image-cache.yaml
target=$(yq 'select(.kind == "CiliumClusterwideNetworkPolicy") | .spec.egress[0].toEndpoints[0].matchLabels["k8s:app.kubernetes.io/name"]' "$manifest")
target_ns=$(yq 'select(.kind == "CiliumClusterwideNetworkPolicy") | .spec.egress[0].toEndpoints[0].matchLabels["k8s:io.kubernetes.pod.namespace"]' "$manifest")
mirror=$(yq 'select(.kind == "Deployment") | .spec.template.metadata.labels["app.kubernetes.io/name"]' "$manifest")
mirror_ns=$(yq 'select(.kind == "Deployment") | .metadata.namespace' "$manifest")
[ "$target" = "talos-image-cache" ]
[ "$target_ns" = "kube-system" ]
# The allow's destination must equal the mirror Deployment's own pod label and
# namespace, otherwise the hole is punched toward nothing: moving the mirror
# would leave these tests green while importers could no longer reach it.
[ "$target" = "$mirror" ]
[ "$target_ns" = "$mirror_ns" ]
}
@test "strict 206 gate selects the mirror on a byte-range success" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
if ! _talos_image_cache_probe_succeeded "code=206"; then
echo "expected a 206 probe result to select the mirror" >&2
exit 1
fi
}
@test "strict 206 gate finds the marker inside surrounding probe output" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
out=$(printf 'pod/talos-image-cache-probe created\ncode=206\npod "talos-image-cache-probe" deleted\n')
if ! _talos_image_cache_probe_succeeded "$out"; then
echo "expected 206 to be found in multi-line attach output" >&2
exit 1
fi
}
@test "strict 206 gate falls back on range-less, unreachable, empty and unrelated output" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
# A plain 200 means the mirror answered without range support; 000 means curl
# never connected. Both fall back to the public factory, as does output that
# carried no result marker at all (an attach stream lost with no logs to recover).
for bad in "code=200" "code=000" "code=404" "" "error: pods not found"; do
if _talos_image_cache_probe_succeeded "$bad"; then
echo "expected non-206 output to fall back to the public factory: [$bad]" >&2
exit 1
fi
done
}
_stub_kubectl_dir() {
# Build a throwaway PATH entry whose `kubectl get deploy ... -o name` fails
# its first $2 invocations and then answers with $3 on stdout (empty for a
# genuine absence). $1 is the scratch dir. printf, not a heredoc: cozytest.sh
# translates the file with awk and would read a @test or a bare `}` inside a
# heredoc as real syntax.
mkdir -p "$1/bin"
printf '%s\n' \
'#!/bin/sh' \
'printf "%s\n" "$*" >> "$STUB_CALLS"' \
'n=$(cat "$STUB_COUNTER")' \
'n=$((n + 1))' \
'printf "%s" "$n" > "$STUB_COUNTER"' \
'if [ "$n" -le "$STUB_FAILS" ]; then' \
' echo "error: the server was unable to return a response" >&2' \
' exit 1' \
'fi' \
'printf "%s" "$STUB_OUT"' \
> "$1/bin/kubectl"
chmod +x "$1/bin/kubectl"
# A transparent `timeout` that records how it was invoked and then runs the
# command anyway, so a test can pin the wrapper without changing what the
# wrapped call does. Same idiom as hack/run-kubernetes-serial-console_test.bats.
# With STUB_TIMEOUT_RC set it instead exits that status without running the
# command and without printing, which is what a real kill looks like from the
# caller's side: the wrapper says nothing and the command never reaches the
# point where it would have.
printf '%s\n' \
'#!/bin/sh' \
'printf "%s\n" "$*" >> "$STUB_TIMEOUT_CALLS"' \
'if [ -n "$STUB_TIMEOUT_RC" ]; then exit "$STUB_TIMEOUT_RC"; fi' \
'while [ $# -gt 0 ] && [ "$1" != kubectl ]; do shift; done' \
'exec "$@"' \
> "$1/bin/timeout"
chmod +x "$1/bin/timeout"
printf '0' > "$1/counter"
: > "$1/calls"
: > "$1/timeout.calls"
export STUB_COUNTER="$1/counter" STUB_CALLS="$1/calls" \
STUB_TIMEOUT_CALLS="$1/timeout.calls" STUB_FAILS="$2" STUB_OUT="$3" \
STUB_TIMEOUT_RC=""
PATH="$1/bin:$PATH"
}
@test "deployment lookup reports present when the mirror answers with a name" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
d=$(mktemp -d)
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 0 'deployment.apps/talos-image-cache'
state=$(_talos_image_cache_deploy_state)
rm -rf "$d"
[ "$state" = present ]
}
@test "deployment lookup reports absent only on an empty answer the apiserver actually gave" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
d=$(mktemp -d)
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
# --ignore-not-found: a real absence is exit 0 with no output. This is the
# one outcome allowed to turn the whole suite toward the public factory.
_stub_kubectl_dir "$d" 0 ''
state=$(_talos_image_cache_deploy_state)
# The flag has to be pinned by inspection, because the stub gives exit 0 and
# no output whether or not the call asks for it. Drop it from the real call
# and kubectl reports a genuine absence as an error instead: `absent` becomes
# a state production can never reach, while this test keeps passing.
flagged=$(grep -c -- '--ignore-not-found' "$d/calls" || true)
rm -rf "$d"
[ "$state" = absent ]
[ "$flagged" = 1 ]
}
@test "deployment lookup reports unknown when the apiserver never answers" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
d=$(mktemp -d)
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
# Every attempt refused. Reading this as "absent" is the defect: a blip would
# send every tenant worker in the suite to the public factory.
_stub_kubectl_dir "$d" 99 'deployment.apps/talos-image-cache'
state=$(_talos_image_cache_deploy_state)
rm -rf "$d"
[ "$state" = unknown ]
}
@test "deployment lookup rides out a blip and reports what a later attempt found" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
d=$(mktemp -d)
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 1 'deployment.apps/talos-image-cache'
state=$(_talos_image_cache_deploy_state)
tries=$(cat "$d/counter")
rm -rf "$d"
[ "$state" = present ]
[ "$tries" = 2 ]
}
@test "every attempt at the lookup carries a request deadline" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
d=$(mktemp -d)
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 99 'deployment.apps/talos-image-cache'
state=$(_talos_image_cache_deploy_state)
# kubectl's --request-timeout defaults to 0, so a connection that
# establishes and then stalls hangs with no deadline, and re-asking would
# multiply that hang instead of riding out a blip. The flag alone is not a
# wall-clock bound either — kubectl retries discovery several times over
# before it gives up — so each attempt needs the wrapper as well. The last
# count pins the number of attempts, not their duration.
bounded=$(grep -c -- '--request-timeout=30s' "$d/calls" || true)
# The wall-clock bound is deliberately above the request deadline beside it:
# level with it, the kill lands first every time and --request-timeout can
# never fire, which is how the one silent failure mode stays silent.
wrapped=$(grep -c '^-k 5 40 kubectl ' "$d/timeout.calls" || true)
total=$(grep -c . "$d/calls" || true)
rm -rf "$d"
[ "$state" = unknown ]
[ "$total" = 3 ]
[ "$bounded" = 3 ]
[ "$wrapped" = 3 ]
}
@test "a present mirror routes the decision into the reachability probe" {
d=$(mktemp -d)
_TALOS_IMAGE_FACTORY_DECISION_FILE="$d/decision"
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 0 'deployment.apps/talos-image-cache'
set +x
url=$(resolve_talos_image_factory_url 2>"$d/err")
err=$(cat "$d/err")
rm -rf "$d"
# The state token is a contract between this helper and its two readers.
# Drift on the resolve side is the worst case there is: the present branch
# would fall through to "not deployed", cache an empty URL, and send every
# tenant worker in the suite to the public factory — the very harm the
# three-way answer exists to prevent, with every other test still green.
[ -z "$url" ]
printf '%s' "$err" | grep -q 'Available but a tenant-scoped probe' || { echo "a present mirror must reach the probe, not the not-deployed branch: [$err]" >&2; exit 1; }
}
@test "an undecidable lookup warns and is not cached for the rest of the suite" {
d=$(mktemp -d)
_TALOS_IMAGE_FACTORY_DECISION_FILE="$d/decision"
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 99 'deployment.apps/talos-image-cache'
# cozytest.sh runs each test under `set -x`, and xtrace shares the stderr
# this captures. Off for the call, so the assertions below read what the
# function said rather than an echo of the commands it ran, and left off
# afterwards: a test that turns it back on and then fails wedges `bats`
# instead of reporting, which costs a reader the failure they came for.
set +x
url=$(resolve_talos_image_factory_url 2>"$d/err")
cached=no
if [ -f "$d/decision" ]; then cached=yes; fi
err=$(cat "$d/err")
rm -rf "$d"
# Falling back for this one caller is fine; pinning that fallback for every
# later tenant test on the strength of a failed question is not.
[ -z "$url" ]
[ "$cached" = no ]
printf '%s' "$err" | grep -q 'could not determine' || { echo "an undecidable lookup must say so: [$err]" >&2; exit 1; }
# And it must say why. "could not determine" alone leaves an operator with a
# new silent failure in place of the old one: refused, Unauthorized and
# timed-out all read the same until kubectl's own message reaches the log.
printf '%s' "$err" | grep -q 'the server was unable to return a response' || { echo "the reason the lookup failed must reach the log: [$err]" >&2; exit 1; }
}
@test "a lookup killed by the wall-clock bound still accounts for itself" {
d=$(mktemp -d)
_TALOS_IMAGE_FACTORY_DECISION_FILE="$d/decision"
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 0 'deployment.apps/talos-image-cache'
# The stall this loop exists for, and the one case where nobody speaks:
# `timeout` prints nothing when it fires, and the kubectl it killed never
# reached the point where it would have printed either. The sibling test
# below pins the reason on a kubectl that failed loudly, which every stub
# does by default -- so without this case "the reason reaches the log" is
# pinned only where it was never in doubt.
export STUB_TIMEOUT_RC=124
set +x
url=$(resolve_talos_image_factory_url 2>"$d/err")
err=$(cat "$d/err")
spoke=$(grep -c . "$d/calls" || true)
rm -rf "$d"
[ -z "$url" ]
# kubectl never ran, so anything the warning points at has to come from the
# lookup itself. Drop this and the case degrades into the sibling below.
[ "$spoke" = 0 ]
printf '%s' "$err" | grep -q 'the last one exited 124' || { echo "a lookup cut off by its own bound must say so: [$err]" >&2; exit 1; }
}
@test "a genuine absence still resolves once and stays cached" {
d=$(mktemp -d)
_TALOS_IMAGE_FACTORY_DECISION_FILE="$d/decision"
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 0 ''
set +x
url=$(resolve_talos_image_factory_url 2>"$d/err")
cached=no
if [ -f "$d/decision" ]; then cached=yes; fi
err=$(cat "$d/err")
rm -rf "$d"
[ -z "$url" ]
[ "$cached" = yes ]
printf '%s' "$err" | grep -q 'not deployed' || { echo "a real absence must report itself as such: [$err]" >&2; exit 1; }
}
@test "with timeout off PATH the lookup still asks instead of answering unknown" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
d=$(mktemp -d)
_TALOS_IMAGE_CACHE_QUERY_DELAY=0
_stub_kubectl_dir "$d" 0 'deployment.apps/talos-image-cache'
rm -f "$d/bin/timeout"
# Everything the kubectl stub itself needs, staged from known directories so
# the stripped PATH below loses `timeout` and nothing else. Resolved this way
# rather than through `command -v` for the reason the same idiom gives in
# hack/run-kubernetes-node-join_test.bats: a name mocked as a shell function
# answers with the function and no link gets made.
for c in cat sleep; do
for p in /bin /usr/bin /usr/local/bin /opt/homebrew/bin; do
if [ -x "$p/$c" ]; then ln -sf "$p/$c" "$d/bin/$c"; break; fi
done
done
if [ ! -x "$d/bin/cat" ]; then
echo "could not stage a real cat in the stripped PATH; the check below would be vacuous" >&2
rm -rf "$d"
exit 1
fi
# Stripped for the call and restored right after: the assertions below need a
# `grep` this PATH deliberately does not have.
oldpath=$PATH
PATH="$d/bin"
state=$(_talos_image_cache_deploy_state)
PATH=$oldpath
wrapped=$(grep -c . "$d/timeout.calls" || true)
rm -rf "$d"
# Calling the wrapper anyway when it is not installed makes every attempt
# exit 127, and three of those read as an apiserver that never answered: a
# missing local binary would pin the whole suite to the public factory while
# every message blamed the cluster. Asserting the state rather than the exit
# is what distinguishes the two — 127 lands on `unknown`, not on an error.
[ "$state" = present ]
[ "$wrapped" = 0 ]
}
@test "probe overrides builder emits valid JSON with the image, command and restricted PSA" {
. hack/e2e-chainsaw/_lib/talos-image-cache.sh
img=example.invalid/img:tag
cmd='curl -s -o /dev/null -w code=%{http_code} -r 0-0 http://talos-image-cache.kube-system.svc/x.raw.xz'
json=$(_talos_image_cache_probe_overrides "$img" "$cmd")
# Invalid JSON would make every probe fail and silently drop CI back to the
# public factory, so parse it rather than pattern-match it.
printf '%s' "$json" | yq -p=json -o=json '.' >/dev/null
[ "$(printf '%s' "$json" | yq -p=json '.spec.containers[0].name')" = "c" ]
[ "$(printf '%s' "$json" | yq -p=json '.spec.containers[0].image')" = "$img" ]
[ "$(printf '%s' "$json" | yq -p=json '.spec.containers[0].command[2]')" = "$cmd" ]
[ "$(printf '%s' "$json" | yq -p=json '.spec.securityContext.runAsNonRoot')" = "true" ]
[ "$(printf '%s' "$json" | yq -p=json '.spec.containers[0].securityContext.allowPrivilegeEscalation')" = "false" ]
}