mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-26 21:17:32 +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>
380 lines
16 KiB
Bash
380 lines
16 KiB
Bash
#!/usr/bin/env bats
|
|
# Behavioural tests for the multus install-cni-plugins init container script:
|
|
# extracted from the rendered DaemonSet and run. The helm-unittest cases in
|
|
# packages/system/multus/tests/multus_test.yaml match its source TEXT instead.
|
|
#
|
|
# Run from the repo root through `make bats-unit-tests`; relative paths resolve
|
|
# against that cwd. The file also remains compatible with the legacy
|
|
# `hack/cozytest.sh` translator, so each @test builds and removes its fixture
|
|
# inline instead of relying on setup()/teardown().
|
|
#
|
|
# Test-level EXIT traps replace Bats' own handler and hide failing TAP results.
|
|
# Cleanup follows aborting assertions; see docs/agents/e2e-testing.md.
|
|
# Traps in the script under test run in a separate shell and remain in scope.
|
|
|
|
load test_helper
|
|
|
|
CHART=packages/system/multus
|
|
|
|
# Extract the init container's script from the rendered chart into $1 and rebind
|
|
# its two absolute paths ($2 = host cni bin, $3 = plugin source) so it can run
|
|
# unprivileged. The non-empty check must be an early `return 1`: hack/cozytest.sh
|
|
# rewrites every line matching ^}$ into `return 0` plus `}`, helpers included.
|
|
render_script() {
|
|
out=$1
|
|
# The chart's own default is off -- the platform turns it on per bundle -- so
|
|
# the init container this file tests only exists when it is set here.
|
|
helm template --set stageCniPlugins=true "$CHART" \
|
|
| yq eval 'select(.kind == "DaemonSet") | .spec.template.spec.initContainers[] | select(.name == "install-cni-plugins") | .command[2]' - \
|
|
| sed -e "s|/host/opt/cni/bin|$2|g" -e "s|/cni-plugins|$3|g" \
|
|
-e "s|/dev/termination-log|${out}.termlog|g" > "$out"
|
|
[ -s "$out" ] || return 1
|
|
|
|
# Nothing may still point at a real /opt/cni/bin: the rebinding substitutes
|
|
# /host/opt/cni/bin literally, and these tests EXECUTE the script.
|
|
if grep -q '/opt/cni/bin' "$out"; then
|
|
echo "rendered script still references a real /opt/cni/bin after rebinding;" >&2
|
|
echo "refusing to execute it. Check that the manifest still writes through" >&2
|
|
echo "/host/opt/cni/bin, which is what this rebinding substitutes." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# The names the image actually stages, read from the Dockerfile rather than
|
|
# repeated here: a script that skips a plugin by name is only exercised against
|
|
# a fixture that contains that name.
|
|
plugin_names() {
|
|
sed -n '/tar -xz -C \/cni-plugins -f /,/;/p' \
|
|
packages/system/multus/images/multus-cni/Dockerfile |
|
|
tr ' ' '\n' | sed -n 's|^\./||p' | tr -d ';' | sort
|
|
}
|
|
|
|
# A fake image payload: plugin binaries that report their own name when run.
|
|
make_plugins() {
|
|
mkdir -p "$1"
|
|
for p in $(plugin_names); do
|
|
printf '#!/bin/sh\necho %s-NEW\n' "$p" > "$1/$p"
|
|
chmod 0755 "$1/$p"
|
|
done
|
|
}
|
|
|
|
# Put an `rm` in $1 that refuses any path containing $2, leaving it in place, and
|
|
# delegates the rest. Injected rather than provoked with permissions: root's DAC
|
|
# override unlinks through a forbidding parent, so a filesystem-shaped fixture
|
|
# stops reproducing wherever the suite runs as root. $2 is narrow so staging can
|
|
# still succeed around one bad path.
|
|
stub_failing_rm() {
|
|
mkdir -p "$1"
|
|
sed -e "s|@MATCH@|$2|" > "$1/rm" <<'STUB'
|
|
#!/bin/sh
|
|
for a in "$@"; do
|
|
case $a in
|
|
*@MATCH@*)
|
|
echo "rm: $a: Read-only file system" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
exec /bin/rm "$@"
|
|
STUB
|
|
chmod 0755 "$1/rm"
|
|
}
|
|
|
|
@test "a render that produces no script fails instead of passing as a no-op" {
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/dst"
|
|
# Guards the guard. Two tests below assert nothing landed in the destination,
|
|
# so an unrendered script satisfies them. This also fails if the check is
|
|
# moved into last-command position, where cozytest.sh's `return 0` rewrite
|
|
# swallows its status. Each @test runs in its own subshell, so reassigning
|
|
# CHART cannot leak.
|
|
CHART=$tmp/no-such-chart
|
|
if render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src" 2>/dev/null; then
|
|
echo "FAIL: render_script reported success with no chart to render"
|
|
false
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "installs every staged plugin into the host cni bin dir, executable" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
sh "$tmp/s.sh"
|
|
|
|
names=$(plugin_names)
|
|
[ -n "$names" ]
|
|
[ "$(printf '%s\n' "$names" | grep -c .)" = "14" ]
|
|
[ "$(ls -1 "$tmp/dst" | wc -l | tr -d ' ')" = "14" ]
|
|
# Each by name: a count is satisfied by that many of anything, and one plugin
|
|
# landing unexecutable leaves the others to carry the assertion.
|
|
for p in $names; do
|
|
[ -f "$tmp/dst/$p" ]
|
|
[ -x "$tmp/dst/$p" ]
|
|
[ "$("$tmp/dst/$p")" = "$p-NEW" ]
|
|
done
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "installs a plugin executable even when the staged copy is not" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# cp reproduces the source mode, so the chmod is only load-bearing when a
|
|
# staged plugin is not already 0755.
|
|
chmod 0644 "$tmp/src/bridge"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
sh "$tmp/s.sh"
|
|
|
|
# Two statements, not `[ -f X ] && [ -x X ]`: a failing command that is not the
|
|
# last in an AND-OR list is exempt from errexit, so the pair silently continues
|
|
# into cozytest.sh's appended `return 0` and the whole assertion is discarded.
|
|
# Measured in dash and bash -- the `-f` half of every such pair was dead.
|
|
[ -f "$tmp/dst/bridge" ]
|
|
[ -x "$tmp/dst/bridge" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "replaces a plugin by rename, not by writing onto the live path" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# Stand in for a plugin another CNI already installed and the kubelet may exec.
|
|
printf '#!/bin/sh\necho portmap-OLD\n' > "$tmp/dst/portmap"
|
|
chmod 0755 "$tmp/dst/portmap"
|
|
before=$(ls -i "$tmp/dst/portmap" | awk '{print $1}')
|
|
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
sh "$tmp/s.sh"
|
|
|
|
after=$(ls -i "$tmp/dst/portmap" | awk '{print $1}')
|
|
[ "$("$tmp/dst/portmap")" = "portmap-NEW" ]
|
|
# A rename swaps in a new inode, so an exec holding the old one keeps a
|
|
# complete binary; a cp onto the live path truncates and rewrites in place.
|
|
if [ "$before" = "$after" ]; then echo "FAIL: portmap replaced in place (inode $before unchanged) — copy was not atomic"; false; fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "leaves no temp files behind, and clears ones stranded by an earlier kill" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# Named for a plugin this image does not stage, so no iteration renames it
|
|
# away -- only the cleanup can remove it.
|
|
echo stranded > "$tmp/dst/.tmp-cozystack-multus-plugin-from-an-older-image"
|
|
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
sh "$tmp/s.sh"
|
|
|
|
[ "$(ls -a1 "$tmp/dst" | grep -c '^\.tmp-cozystack-multus-' || true)" = "0" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "skips staging instead of failing when the image predates the plugins" {
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/dst"
|
|
# No source dir at all: the release-prep digest re-pin lags a Dockerfile
|
|
# change, so the pinned image can have no /cni-plugins.
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/absent"
|
|
sh "$tmp/s.sh" 2>"$tmp/err"
|
|
|
|
[ "$(ls -1 "$tmp/dst" | wc -l | tr -d ' ')" = "0" ]
|
|
# The empty destination alone is satisfied by the guard being gone: the glob
|
|
# stays literal and `[ -e "$plugin" ]` skips it. Only this line separates them.
|
|
grep -q 'in this image; skipping staging' "$tmp/err"
|
|
# And nothing may be reported as failed: a skip is not a partial install.
|
|
if grep -q 'failed to install:' "$tmp/err"; then
|
|
echo "FAIL: an absent plugin directory was reported as failed installs"
|
|
cat "$tmp/err"
|
|
false
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "does not fail when the staged plugin directory is empty" {
|
|
tmp=$(mktemp -d)
|
|
mkdir -p "$tmp/src" "$tmp/dst"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
# An unmatched glob stays literal in sh, so without the `[ -e ]` guard the loop
|
|
# runs once for a path that does not exist and reports a plugin named `*`.
|
|
sh "$tmp/s.sh" 2>"$tmp/err"
|
|
|
|
[ "$(ls -1 "$tmp/dst" | wc -l | tr -d ' ')" = "0" ]
|
|
# Asserted, because the empty destination alone is satisfied by the guard
|
|
# being gone.
|
|
if grep -q 'failed to install:' "$tmp/err"; then
|
|
echo "FAIL: an empty plugin directory was reported as failed installs"
|
|
cat "$tmp/err"
|
|
false
|
|
fi
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a leftover directory is cleared, and the plugin installs as a file" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# The temp path as an ordinary directory `rm -rf` can clear: a previous run
|
|
# killed between the cp and the rename. Staging proceeds.
|
|
#
|
|
# The assertions check for a regular FILE: `-x` alone is true of a directory.
|
|
mkdir -p "$tmp/dst/.tmp-cozystack-multus-bridge"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
sh "$tmp/s.sh"
|
|
|
|
[ -f "$tmp/dst/bridge" ]
|
|
[ -x "$tmp/dst/bridge" ]
|
|
[ "$("$tmp/dst/bridge")" = "bridge-NEW" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a temp path that will not clear is refused, not published as a plugin" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# A temp path that is still a directory when the copy runs: cp writes INTO it,
|
|
# chmod works on it, and mv publishes it as the plugin, because a rename onto
|
|
# a free name succeeds. The runtime cannot exec a directory.
|
|
mkdir -p "$tmp/dst/.tmp-cozystack-multus-bridge"
|
|
stub_failing_rm "$tmp/bin" ".tmp-cozystack-multus-bridge"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
|
|
# Still must not fail the container. hostNetwork pods bypass CNI and would
|
|
# start either way; every other pod on the node would not.
|
|
if ! PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" 2>"$tmp/err"; then
|
|
echo "FAIL: an unclearable temp path failed the container"
|
|
cat "$tmp/err"
|
|
false
|
|
fi
|
|
if [ -d "$tmp/dst/bridge" ]; then
|
|
echo "FAIL: a directory was published as the plugin"
|
|
ls -la "$tmp/dst/bridge"
|
|
false
|
|
fi
|
|
# Refused, so nothing is there at all -- not a half-installed something.
|
|
if [ -e "$tmp/dst/bridge" ]; then
|
|
echo "FAIL: expected bridge to be absent after a refusal"
|
|
false
|
|
fi
|
|
# The injection has to have happened: if PATH stopped being honoured, the real
|
|
# `rm` would clear the fixture and every assertion above would still pass.
|
|
if [ ! -d "$tmp/dst/.tmp-cozystack-multus-bridge" ]; then
|
|
echo "FAIL: the temp path was removed, so no failure was injected;"
|
|
echo " this test proved nothing. Check that stub_failing_rm is on PATH"
|
|
echo " and that the script still invokes rm by bare name."
|
|
false
|
|
fi
|
|
# And WHICH plugin: anchored at line start, matched as a whole token. The stub
|
|
# prints the bridge path itself, `.*bridge` would also accept `bridgeport`,
|
|
# and without `^` the contradictory `not failed to install:` passes too.
|
|
grep -qE '^failed to install:( [^ ]+)* bridge( |$)' "$tmp/err"
|
|
# The termination log is the channel the stub cannot write to, so assert there
|
|
# too -- it is also what `kubectl describe` shows.
|
|
grep -qE '^failed to install:( [^ ]+)* bridge( |$)' "$tmp/s.sh.termlog"
|
|
# The other plugins are unaffected: one bad temp path is not a stop signal.
|
|
[ -f "$tmp/dst/macvlan" ]
|
|
[ -x "$tmp/dst/macvlan" ]
|
|
[ "$("$tmp/dst/macvlan")" = "macvlan-NEW" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "refuses to nest a plugin inside a directory that already holds its name" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# mv moves its source INTO an existing directory, under the SOURCE's basename
|
|
# -- the temp name, never <dst>/bridge/bridge. Assert the directory gained no
|
|
# entries: a temp file one level down is deeper than the trap's glob.
|
|
mkdir -p "$tmp/dst/bridge/occupied"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
sh "$tmp/s.sh" 2>"$tmp/err"
|
|
|
|
nested=$(ls -A "$tmp/dst/bridge" | grep -v '^occupied$' || :)
|
|
if [ -n "$nested" ]; then
|
|
echo "FAIL: something was moved inside the existing directory: $nested"
|
|
false
|
|
fi
|
|
# One line, summary and name together: as two greps this passes when the
|
|
# summary names a different plugin and `mv` merely printed this one.
|
|
grep -qE '^failed to install:( [^ ]+)* bridge( |$)' "$tmp/err"
|
|
# And a CAUSE: this is the one branch that records a failure no command
|
|
# printed an error for, so without this line the summary is unexplained.
|
|
grep -q 'is a directory, not a plugin binary' "$tmp/err"
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "survives a leftover it cannot clean up" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# A leftover only the trap's glob reaches. Its `rm` is guarded because a
|
|
# command failing inside an EXIT trap sets the status even after `exit 0`.
|
|
mkdir -p "$tmp/dst/.tmp-cozystack-multus-stranded"
|
|
stub_failing_rm "$tmp/bin" "stranded"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
|
|
if ! PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" 2>"$tmp/err"; then
|
|
echo "FAIL: a leftover that could not be removed failed the container;"
|
|
echo " every pod that needs CNI would then fail to start here"
|
|
cat "$tmp/err"
|
|
false
|
|
fi
|
|
# The leftover has to have SURVIVED: that is the only proof the failure was
|
|
# injected, since a real `rm -rf` would clear it and staging would succeed.
|
|
if [ ! -d "$tmp/dst/.tmp-cozystack-multus-stranded" ]; then
|
|
echo "FAIL: the stranded leftover was removed, so no failure was injected;"
|
|
echo " this test proved nothing about the EXIT trap's guard."
|
|
false
|
|
fi
|
|
# And the staging still has to have happened.
|
|
[ -f "$tmp/dst/bridge" ]
|
|
[ -x "$tmp/dst/bridge" ]
|
|
[ "$("$tmp/dst/bridge")" = "bridge-NEW" ]
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "reports a plugin it could not install, and still leaves the node usable" {
|
|
tmp=$(mktemp -d)
|
|
make_plugins "$tmp/src"; mkdir -p "$tmp/dst"
|
|
# An entry cp cannot copy, sorted ahead of the good plugins so a later
|
|
# iteration still runs.
|
|
mkdir -p "$tmp/src/aaa-uncopyable"
|
|
# Plus a leftover the trap cannot remove, so both failure paths run at once:
|
|
# unguarded, the script dies in the EXIT trap before the summary.
|
|
mkdir -p "$tmp/dst/.tmp-cozystack-multus-stranded"
|
|
stub_failing_rm "$tmp/bin" "stranded"
|
|
render_script "$tmp/s.sh" "$tmp/dst" "$tmp/src"
|
|
|
|
# Both halves: exit 0 alone would pass with the message dropped, and the log
|
|
# line is the only remaining signal.
|
|
if ! PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" 2>"$tmp/err"; then
|
|
echo "FAIL: a plugin that could not be installed failed the container;"
|
|
echo " every pod that needs CNI would then fail to start here"
|
|
false
|
|
fi
|
|
# The script's own wording AND the name, as one line: cp prints the path it
|
|
# could not copy, so either half alone passes with the report deleted.
|
|
if ! grep -qE '^failed to install:( [^ ]+)* aaa-uncopyable( |$)' "$tmp/err"; then
|
|
echo "FAIL: the script did not report the plugin it could not install"
|
|
cat "$tmp/err"
|
|
false
|
|
fi
|
|
|
|
# And into the termination log, which is what `kubectl describe` shows.
|
|
# invisible to anyone not reading that container's logs specifically.
|
|
if ! grep -q 'failed to install:.*aaa-uncopyable' "$tmp/s.sh.termlog" 2>/dev/null; then
|
|
echo "FAIL: the failure was not written to the termination log"
|
|
false
|
|
fi
|
|
|
|
# The stranded leftover has to have survived, or the injected failure never
|
|
# happened and the both-go-wrong path was not exercised.
|
|
if [ ! -d "$tmp/dst/.tmp-cozystack-multus-stranded" ]; then
|
|
echo "FAIL: the stranded leftover was removed, so no rm failure was injected"
|
|
false
|
|
fi
|
|
|
|
# The failure must not take the rest of the set with it.
|
|
[ -f "$tmp/dst/bridge" ]
|
|
[ -x "$tmp/dst/bridge" ]
|
|
[ "$("$tmp/dst/bridge")" = "bridge-NEW" ]
|
|
# And no temp FILE of its own. The planted directory is expected to survive:
|
|
# stub_failing_rm refuses that path, which is what makes the cleanup fail.
|
|
# Counting only regular files keeps the fixture from hiding a real leak.
|
|
[ "$(find "$tmp/dst" -maxdepth 1 -type f -name '.tmp-cozystack-multus-*' | wc -l | tr -d ' ')" = "0" ]
|
|
rm -rf "$tmp"
|
|
}
|