Files
cozystack/hack/cni-plugins-staging-contract.bats
Myasnikov Daniil f3486f4e3a docs: move the operator guides out of docs/ to where readers find them
Five guides in docs/ were written next to the feature code that needed
them and left in the source tree, where no operator looks. They are on
the website, so a second copy here only drifts: edits kept landing in
docs/ after the website copy was made.

docs/vm-external-vlan.md is split rather than moved. Its section on
staging the reference CNI plugins is a contract that
hack/cni-plugins-staging-contract.bats asserts against the multus
Dockerfile, the daemonset manifest and the platform values, so that half
is package documentation and goes to packages/system/multus/README.md,
which the suite now reads. The operator half goes to the website.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-14 16:24:16 +05:00

575 lines
29 KiB
Bash

#!/usr/bin/env bats
# Contract checks for the CNI plugin staging that a helm-unittest case cannot
# make: they read files outside the chart, or render it more than one way.
#
# Run with: bats hack/cni-plugins-staging-contract.bats
load test_helper
# Keep the closing brace on the same line: hack/cozytest.sh rewrites any line
# that is exactly `}` into `return 0` plus `}`, without caring whether it closes
# a test, a helper or a plain brace group at file scope.
command -v yq >/dev/null || { echo "yq is required to read the vendored kube-ovn version" >&2; exit 1; }
# Is $2 one of the plugin names in the extract text $1? Separators differ by
# position in a continued shell line, so fold them to spaces and pad both ends
# -- `./bridge` must not be satisfied by `./bridgeport`.
extract_has() {
case " $(printf '%s' "$1" | tr '\n;\\' ' ') " in
*" ./$2 "*) return 0 ;;
esac
return 1
}
DOCKERFILE="$PWD/packages/system/multus/images/multus-cni/Dockerfile"
KUBEOVN_VALUES="$PWD/packages/system/kubeovn/values.yaml"
DOC="$PWD/packages/system/multus/README.md"
PLATFORM_VALUES="$PWD/packages/core/platform/values.yaml"
TEMPLATE="$PWD/packages/system/multus/templates/multus-daemonset-thick.yml"
PATCHFILE="$PWD/packages/system/multus/patches/customize-deployment.patch"
@test "membership in the extract list is decided the same way in every position" {
middle=' \ ./bridge ./host-local ./vrf; \ '
last=' \ ./bridge ./host-local ./loopback; \ '
first=' \ ./loopback ./bridge ./host-local; \ '
multiline=$(printf ' \\\n ./bridge ./host-local \\\n ./loopback; \\\n')
extract_has "$middle" bridge || { echo "FAIL: missed a name in the middle" >&2; return 1; }
extract_has "$last" loopback || { echo "FAIL: missed a name in last position" >&2; return 1; }
extract_has "$first" loopback || { echo "FAIL: missed a name in first position" >&2; return 1; }
extract_has "$multiline" loopback || { echo "FAIL: missed a name across a line continuation" >&2; return 1; }
if extract_has ' \ ./bridgeport ./host-local; \ ' bridge; then
echo "FAIL: ./bridgeport satisfied a check for ./bridge" >&2
return 1
fi
if extract_has ' \ ./host-local ./bridgeport; \ ' bridge; then
echo "FAIL: ./bridgeport in last position satisfied a check for ./bridge" >&2
return 1
fi
if extract_has "$middle" loopback; then
echo "FAIL: reported a name that is not in the list" >&2
return 1
fi
}
@test "the Dockerfile stages the plugins and copies them into the final image" {
grep -qE '^[[:space:]]*tar -xz -C /cni-plugins -f ' "$DOCKERFILE"
# The COPY has to land in the LAST stage: one in an earlier stage satisfies
# the same pattern while the image that ships carries no plugins at all, and
# the init container then skips on every node.
final_stage=$(awk '/^FROM/{buf=""} {buf = buf $0 ORS} END{printf "%s", buf}' "$DOCKERFILE")
if [ -z "$final_stage" ]; then
echo "no FROM in the Dockerfile" >&2
return 1
fi
if ! printf '%s' "$final_stage" |
grep -qE '^COPY[[:space:]]+--from=build[[:space:]]+/cni-plugins[[:space:]]+/cni-plugins[[:space:]]*$'; then
echo "the final stage does not COPY /cni-plugins, so the plugins are built" >&2
echo "and then left behind in the build stage." >&2
grep -nE '^FROM|^COPY.*cni-plugins' "$DOCKERFILE" >&2
return 1
fi
# And nothing after it may take them out again: the COPY being present says
# nothing about the directory still existing when the build ends.
after_copy=$(printf '%s' "$final_stage" |
sed -n '/^COPY[[:space:]]\{1,\}--from=build[[:space:]]\{1,\}\/cni-plugins[[:space:]]/,$p')
if printf '%s' "$after_copy" | grep -qE '^[^#]*\brm\b.*/cni-plugins'; then
echo "something in the final stage removes /cni-plugins after it was" >&2
echo "copied in, so the image ships without the plugins and the init" >&2
echo "container skips on every node." >&2
printf '%s' "$after_copy" | grep -nE '^[^#]*\brm\b.*/cni-plugins' >&2
return 1
fi
# And nothing anywhere in the file may delete a staged plugin: the check above
# only reads the tar arguments, so a later `rm -f /cni-plugins/bridge` in the
# build stage ships an image with thirteen. The tarball itself is a different
# path (/tmp/cni-plugins.tgz) and removing it is expected.
if grep -qE '\brm\b[^#]*[^.[:alnum:]]/cni-plugins(/|[[:space:]]|;|$)' "$DOCKERFILE"; then
echo "the Dockerfile removes something from /cni-plugins; whatever is" >&2
echo "taken out there is missing from the image, and the init container" >&2
echo "installs only what remains." >&2
grep -nE '\brm\b[^#]*/cni-plugins' "$DOCKERFILE" >&2
return 1
fi
# The pinned digest must still BE the pinned digest when the comparison runs.
# Assigning ${sha256} from the downloaded file turns the verification into a
# comparison of the download with itself, which any well-formed archive passes.
assigns=$(grep -cE '^[[:space:]]*[^#]*\bsha256=' "$DOCKERFILE")
pinned=$(grep -cE '^[[:space:]]+[a-z0-9]+\)[[:space:]]+sha256="[0-9a-f]{64}"' "$DOCKERFILE")
if [ "$assigns" != "$pinned" ]; then
echo "sha256 is assigned $assigns times but only $pinned of those are the" >&2
echo "per-architecture pins. Anything else reassigns the digest before the" >&2
echo "comparison, which then checks the download against itself." >&2
grep -nE 'sha256=' "$DOCKERFILE" >&2
return 1
fi
# The uid the init container runs as is decided in three places: the pod
# securityContext, the container securityContext and the image's own USER.
# The first two are asserted in packages/system/multus/tests/multus_test.yaml;
# this is the third. A non-root USER in the final stage makes every write fail
# while both Kubernetes levels still read as unset, and the script then
# reports the failures, exits 0 and leaves the pod Ready with nothing staged.
final_user=$(printf '%s' "$final_stage" | grep -E '^USER[[:space:]]' || true)
case "$final_user" in
"") ;;
*"USER root"*|*"USER 0"*) ;;
*)
echo "the final stage sets a non-root USER:" >&2
printf '%s\n' "$final_user" >&2
echo "the init container writes into a hostPath and needs uid 0." >&2
return 1 ;;
esac
# Only the plugins may land in /cni-plugins. The init container installs every
# file it finds there and chmods it 0755, so anything else that arrives -- a
# licence, a README, a stray COPY -- is published into the host CNI directory
# as an executable. Measured: adding the licence to that directory makes the
# script install 15 files with LICENSE among them.
strays=$(grep -nE '^(COPY|ADD)[[:space:]].*[[:space:]]/cni-plugins(/|[[:space:]]*$)' "$DOCKERFILE" |
grep -v 'COPY --from=build /cni-plugins /cni-plugins' || true)
if [ -n "$strays" ]; then
echo "something other than the plugin directory itself is copied into" >&2
echo "/cni-plugins; the init container would install it on every node as" >&2
echo "an executable CNI plugin." >&2
printf '%s\n' "$strays" >&2
return 1
fi
# Apache-2.0 binaries are redistributed here, so the licence ships with them.
# Terminated on its own line for the same reason as the checksum: a command
# chained after the `;` runs inside the same RUN and can undo it.
grep -qE '^[[:space:]]*tar -xz -C /cni-plugins-licenses -f [^[:space:]]+ \./LICENSE;[[:space:]]*\\?[[:space:]]*$' "$DOCKERFILE"
printf '%s' "$final_stage" |
grep -qE '^COPY[[:space:]]+--from=build[[:space:]]+/cni-plugins-licenses[[:space:]]+/cni-plugins-licenses[[:space:]]*$'
# Matched inside the extract's own continued line, so a mention elsewhere in
# the file cannot satisfy it.
extract=$(sed -n '/tar -xz -C \/cni-plugins -f /,/;/p' "$DOCKERFILE" | tr '\n' ' ')
[ -n "$extract" ]
for plugin in loopback dhcp dummy tap; do
if extract_has "$extract" "$plugin"; then
echo "packages/system/multus/images/multus-cni/Dockerfile now extracts" >&2
echo "./$plugin, which this package deliberately does not stage, and" >&2
echo "which packages/system/multus/README.md names as left alone." >&2
if [ "$plugin" = loopback ]; then
echo "The init container would then replace the node's loopback plugin" >&2
echo "on every restart. The runtime calls loopback for every pod" >&2
echo "sandbox, not just those using a NAD, so a bad copy costs the" >&2
echo "whole node rather than one attachment -- and cilium and kube-ovn" >&2
echo "already install it, so staging adds a writer for no gain." >&2
elif [ "$plugin" = dhcp ]; then
echo "dhcp is not usable from the binary alone: it needs a" >&2
echo "long-running daemon and a socket on the host, which this" >&2
echo "package does not run." >&2
fi
echo "Update the multus README in the same commit if this is deliberate." >&2
return 1
fi
done
# Nothing may take the failure away: tar returns non-zero when a requested
# member is absent from the archive, and `|| true` there ships an image
# missing that plugin with a successful build. The range ends at the first
# `;`, so a chain before it is inside what is checked.
if printf '%s' "$extract" | grep -qE '\|\||&&|\|'; then
echo "the plugin extraction is chained with another command, which takes" >&2
echo "its failure away: a release archive missing one requested member" >&2
echo "would then build a successful image without that plugin." >&2
printf '%s\n' "$extract" >&2
return 1
fi
# The staged set by NAME, not a count: a count is satisfied by one name
# appearing twice while another is missing.
staged=$(printf '%s' "$extract" | tr ' ' '\n' | sed -n 's|^\./||p' | tr -d ';' | sort)
expected=$(printf '%s\n' bandwidth bridge firewall host-device host-local ipvlan \
macvlan portmap ptp sbr static tuning vlan vrf | sort)
if [ "$staged" != "$expected" ]; then
echo "the set of plugins the Dockerfile stages has changed." >&2
echo "staged now:" >&2
printf '%s\n' "$staged" >&2
echo "expected these 14:" >&2
printf '%s\n' "$expected" >&2
echo "packages/system/multus/README.md tells operators the package installs 14 of" >&2
echo "the upstream reference plugins and names the four it leaves alone," >&2
echo "so that sentence is now wrong. Update it in the same commit." >&2
return 1
fi
for plugin in bridge macvlan ipvlan; do
if ! extract_has "$extract" "$plugin"; then
echo "packages/system/multus/images/multus-cni/Dockerfile no longer extracts" >&2
echo "./$plugin into /cni-plugins, but packages/system/multus/README.md tells operators" >&2
echo "the package stages it. The init container installs whatever is in that" >&2
echo "directory and skips silently when it is absent, so a NAD naming" >&2
echo "$plugin would go back to failing with" >&2
echo "\"failed to find plugin \\\"$plugin\\\" in path [/opt/cni/bin]\"" >&2
echo "on every node, with nothing else in this suite going red." >&2
return 1
fi
done
}
@test "the tarball checksum is present, pinned, and precedes extraction" {
# These are TEXT checks. hack/common-envs.mk builds amd64 and arm64 by
# default, so both per-arch digests below are live on a build without an
# explicit PLATFORM. CI pins linux/amd64, so there only the amd64 one is
# exercised.
# One whole line, and it must TERMINATE. The `;` is what closes the family:
# `|| true`, `&& :`, `| cat` (a pipeline's status is its last element and there
# is no pipefail in POSIX sh), and `|| true` moved to the next physical line,
# since an unterminated command must end in `\`. `${sha256}` in the comparison
# is what stops the tarball being checked against itself.
#
# sha256sum accepts "HASH FILE" and "HASH *FILE"; both are allowed here.
# The file operand is taken from the curl line rather than left open: an
# unpinned operand is satisfied by any path, so a decoy file with a pinned
# digest passes the form while the downloaded tarball is extracted unchecked.
tarball=$(grep -oE '[[:space:]]-o[[:space:]]+[^[:space:]]+' "$DOCKERFILE" | head -1 | awk '{print $2}')
if [ -z "$tarball" ]; then
echo "no 'curl ... -o <file>' in the Dockerfile, so there is no download" >&2
echo "for the checksum to be about." >&2
return 1
fi
tarball_re=$(printf '%s' "$tarball" | sed 's/[].[^$*\\/]/\\&/g')
if ! grep -qE "^[[:space:]]*echo \"\\\$\\{sha256\\}([[:space:]]+|[[:space:]]\\*)${tarball_re}\" \\| sha256sum -c -;[[:space:]]*\\\\?[[:space:]]*$" "$DOCKERFILE"; then
echo "the verify line is not the expected form:" >&2
echo " echo \"\${sha256} ${tarball}\" | sha256sum -c -;" >&2
echo "It must compare the pinned digest against the file curl downloaded," >&2
echo "and must terminate on its own line -- anything chained after it, on" >&2
echo "this line or the next, takes the failure away and an unverified" >&2
echo "tarball is extracted." >&2
grep -nE 'sha256sum' "$DOCKERFILE" >&2
return 1
fi
# And the file that was verified must be the one unpacked.
if ! grep -qE "^[[:space:]]*tar -xz -C /cni-plugins -f ${tarball_re}([[:space:]]|$)" "$DOCKERFILE"; then
echo "the extraction does not unpack ${tarball}, the file the checksum" >&2
echo "above verified, so the verification decides nothing." >&2
grep -nE 'tar -xz' "$DOCKERFILE" >&2
return 1
fi
# `\` joins the physical lines before the shell sees them, so `set +e` on any
# continuation disables the checksum, curl --fail and tar together while
# `RUN set -eux` stays intact.
verify_run=$(awk '
/^RUN / { block = $0; inblock = 1; next }
inblock { block = block "\n" $0 }
inblock && !/\\$/ { if (block ~ /sha256sum -c -/) { print block; exit }; inblock = 0 }
' "$DOCKERFILE")
if [ -z "$verify_run" ]; then
echo "could not find the RUN that verifies the tarball" >&2
return 1
fi
# Errexit IS checked, on the logical line the shell actually runs. Four earlier
# attempts to decide this by grepping physical lines failed, each in its own
# way: `set -ux` (absent), `set +e` later (revoked), `# set -e` (a comment,
# which under a continuation also silences the rest of the joined line), and
# `echo 'set -e'` (an argument, not a command). Joining the continuations and
# cutting every comment first removes all four: what remains is the text the
# shell sees, and errexit has to be its FIRST command.
#
# Both spellings of revocation are rejected: `set +e` and `set +o errexit`.
#
# Not covered, and not claimed to be: errexit reached through a variable or a
# conditional. Deciding that needs a shell, not a pattern.
run_start=$(awk '/^RUN /{r=NR} /sha256sum -c -/{print r; exit}' "$DOCKERFILE")
if [ -z "$run_start" ]; then
echo "no RUN instruction contains the checksum comparison" >&2
return 1
fi
joined=$(tail -n +"$run_start" "$DOCKERFILE" |
awk '{print} !/\\[[:space:]]*$/{exit}' |
sed 's/#.*//' | tr '\n' ' ' | tr -s ' ')
# The same joined line, as the shell parses it. Deleting `case ... in` or its
# `esac` leaves a syntax error the image build would catch -- in minutes, and
# only on a pull request that touches this package. `sh -n` catches it here in
# milliseconds. Continuations are stripped first: with the backslashes left in
# place the shell sees them as literals and even a correct RUN fails to parse.
if ! printf '%s' "$(tail -n +"$run_start" "$DOCKERFILE" |
awk '{print} !/\\[[:space:]]*$/{exit}' |
sed -e 's/^RUN[[:space:]]*//' -e 's/[[:space:]]*\\$//' | tr '\n' ' ')" | sh -n 2>/dev/null; then
echo "the RUN that verifies and extracts the plugins does not parse as a" >&2
echo "shell command once its continuations are joined." >&2
return 1
fi
first=$(printf '%s' "$joined" | sed 's/^RUN[[:space:]]*//' | cut -d';' -f1)
if ! printf '%s' "$first" | grep -qE '^[[:space:]]*set[[:space:]]+-[a-zA-Z]*e'; then
echo "the RUN that verifies the tarball does not begin by enabling errexit." >&2
echo "Its first command is: $first" >&2
echo "Without it a failed checksum is reported and the build continues to" >&2
echo "extract the archive it just rejected." >&2
return 1
fi
if printf '%s' "$joined" | grep -qE 'set[[:space:]]+(\+[a-zA-Z]*e|\+o[[:space:]]+errexit)'; then
echo "errexit is revoked later in the same RUN, which puts the checksum" >&2
echo "back to being advisory." >&2
printf '%s\n' "$joined" >&2
return 1
fi
# Both per-arch pins, each separately: an alternation is satisfied by whichever
# one survived.
for arch in amd64 arm64; do
if ! grep -qE "^[[:space:]]+$arch\)[[:space:]]+sha256=\"[0-9a-f]{64}\"" "$DOCKERFILE"; then
echo "no pinned 64-hex SHA-256 for $arch in the Dockerfile." >&2
echo "That arch would be extracted on the strength of its tag alone, and" >&2
echo "for arm64 no build exercises the path that would notice." >&2
grep -nE 'sha256=' "$DOCKERFILE" >&2
return 1
fi
done
# Verification must precede extraction. Located by the strict form above, not
# by the substring: an inert `: "sha256sum -c -";` earlier in the file would
# satisfy an ordering check anchored on a mention.
v=$(grep -nE '^[[:space:]]*echo "\$\{sha256\}([[:space:]]+|[[:space:]]\*)[^"]+" \| sha256sum -c -;[[:space:]]*\\?[[:space:]]*$' "$DOCKERFILE" | head -1 | cut -d: -f1)
t=$(grep -nE '^[[:space:]]*tar -xz -C /cni-plugins -f ' "$DOCKERFILE" | head -1 | cut -d: -f1)
if [ -z "$v" ] || [ -z "$t" ] || [ "$v" -ge "$t" ]; then
echo "the checksum no longer precedes the extraction (verify line $v, tar line $t)" >&2
return 1
fi
}
@test "the kube-ovn release the CNI plugin pin was checked against is the one vendored" {
# Each side non-empty first: two missing keys compare equal.
checked=$(sed -n 's|^# kube-ovn-cni-plugins-checked-against: *\([^ ]*\) *$|\1|p' "$DOCKERFILE")
[ -n "$checked" ]
vendored=$(yq -r '.global.images.kubeovn.tag' "$KUBEOVN_VALUES" | sed 's|@.*||')
[ -n "$vendored" ]
[ "$vendored" != "null" ]
if [ "$checked" != "$vendored" ]; then
echo "the multus CNI plugin pin was checked against kube-ovn $checked," >&2
echo "but packages/system/kubeovn/values.yaml now vendors $vendored." >&2
echo >&2
echo "Read CNI_PLUGINS_VERSION in kube-ovn $vendored's base image" >&2
echo "(dist/images/Dockerfile.base). If it still matches the" >&2
echo "CNI_PLUGINS_VERSION in packages/system/multus/images/multus-cni/" >&2
echo "Dockerfile, only move the marker there to $vendored. If it does not," >&2
echo "the shared portmap and macvlan would flap between the two daemonsets," >&2
echo "so bring the multus pin and its per-arch checksums across first." >&2
return 1
fi
}
@test "the rendered multus daemon config is valid JSON" {
# Every assertion on this document elsewhere is a pattern over its text, and a
# pattern is happy with trailing garbage: `"multusMasterCNI": "05-cilium.conflist" garbage,`
# keeps them all green while multus gets a config it cannot parse and comes up
# with no master CNI at all. Parsing is the only check that decides this, and
# helm-unittest cannot do it.
#
# It sits here rather than beside the other daemon-config assertions because
# those are helm-unittest cases in packages/system/multus/tests/multus_test.yaml
# and this one needs a parser. This file is where the multus checks that need
# something outside helm-unittest live, staging or not.
cfg=$(helm template packages/system/multus |
yq eval 'select(.kind == "ConfigMap") | .data["daemon-config.json"]' -)
[ -n "$cfg" ]
if ! printf '%s' "$cfg" | yq eval -p json '.' - >/dev/null 2>&1; then
echo "the rendered daemon-config.json does not parse as JSON:" >&2
printf '%s\n' "$cfg" >&2
return 1
fi
# And the key that decides which conflist multus treats as primary has to
# survive parsing, not merely appear in the text.
master=$(printf '%s' "$cfg" | yq eval -p json '.multusMasterCNI' -)
if [ "$master" != "05-cilium.conflist" ]; then
echo "the parsed multusMasterCNI is '$master', not 05-cilium.conflist" >&2
return 1
fi
}
@test "the Talos release the CNI plugin pin was checked against is the one shipped" {
# Each side non-empty first: two missing keys compare equal.
checked=$(sed -n 's|^# talos-cni-plugins-checked-against: *\([^ ]*\) *$|\1|p' "$DOCKERFILE")
[ -n "$checked" ]
shipped=$(awk '/^version:/ {print $2; exit}' packages/core/talos/images/talos/profiles/installer.yaml)
[ -n "$shipped" ]
if [ "$checked" != "$shipped" ]; then
echo "the multus CNI plugin pin was checked against Talos $checked," >&2
echo "but packages/core/talos now ships $shipped." >&2
echo >&2
echo "Read cni_version in siderolabs/pkgs' Pkgfile on the matching release" >&2
echo "branch. If it still equals CNI_PLUGINS_VERSION in this Dockerfile," >&2
echo "only move the marker to $shipped. If it does not, staging would" >&2
echo "replace the node image's bridge, firewall, host-local and portmap" >&2
echo "with a different version of the same plugins on every Talos node," >&2
echo "so decide which version wins before moving it." >&2
return 1
fi
}
@test "the multus README's remediation quotes what the daemonset actually emits" {
# The README names a log line and a container; both spellings live in the
# manifest.
grep -qF -- 'no /cni-plugins in this image; skipping staging' "$TEMPLATE"
grep -qF -- 'no /cni-plugins in this image; skipping staging' "$DOC"
grep -qF -- 'name: install-cni-plugins' "$TEMPLATE"
# Inside the command an operator is told to run, not anywhere in the file: a
# mention left in prose ("older notes may say ...") satisfies a bare match
# while the command itself names a different container.
grep -qE -- 'kubectl [^`]*logs [^`]*--container install-cni-plugins($|[^-[:alnum:]])' "$DOC"
# The count as a reader meets it. Only the staged count is quoted, never the
# upstream total: that is a property of a release this suite cannot read.
grep -qE -- '(^|[^0-9])14 of the upstream reference plugins' "$DOC"
# The half of that sentence naming the four, pinned separately: of the four
# only loopback arrives from anywhere else (cilium and kube-ovn install it).
grep -qF -- 'and leaves `loopback`, `dhcp`, `dummy` and `tap` alone' "$DOC"
if grep -qF -- '`tap` to the node image' "$DOC"; then
echo "the README again attributes the unstaged plugins to the node image;" >&2
echo "staging exists because that image may not carry them" >&2
return 1
fi
if grep -qF -- 'immediately above' "$DOC"; then
echo "the README again promises the cause sits immediately above the" >&2
echo "'failed to install:' summary. It does not when more than one plugin" >&2
echo "fails: the summary is printed once, after the loop." >&2
return 1
fi
grep -qE -- '(^|[^0-9])14 of the upstream reference plugins' "$PLATFORM_VALUES"
grep -qF -- 'loopback, dhcp, dummy and tap are left alone' "$PLATFORM_VALUES"
# Presence only: a grep cannot decide whether prose still MEANS what it says,
# and `do not set the opt-out before upgrading` contains this string. The
# reversal is guarded by name, which closes the spellings seen so far and not
# the general case -- that one stays open deliberately.
grep -qE -- '(^|[^[:alnum:]])set the opt-out before upgrading' "$DOC"
if grep -qiE -- '(do not|never|no need to|dont|don.t) set the opt-out' "$DOC"; then
echo "the README now tells operators NOT to set the opt-out before" >&2
echo "upgrading. Declining afterwards does not restore a replaced plugin," >&2
echo "so the instruction has to stay as it is." >&2
return 1
fi
}
@test "every accepted spelling of stageCniPlugins renders the direction it names" {
# The chart holds the accepted spellings as TWO lists -- one deciding whether
# the value is readable, one deciding whether it means on -- and nothing else
# makes them agree, so each spelling is rendered rather than their union.
#
for v in true TRUE True yes YES on On 1; do
n=$(helm template --set-string stageCniPlugins="$v" packages/system/multus |
yq eval 'select(.kind == "DaemonSet") | .spec.template.spec.initContainers | length' - |
tail -1)
if [ "$n" != "2" ]; then
echo "stageCniPlugins=$v is documented as ON but rendered $n init" >&2
echo "containers (expected 2: install-multus-binary + install-cni-plugins)." >&2
return 1
fi
done
for v in false FALSE False no NO off Off 0; do
n=$(helm template --set-string stageCniPlugins="$v" packages/system/multus |
yq eval 'select(.kind == "DaemonSet") | .spec.template.spec.initContainers | length' - |
tail -1)
if [ "$n" != "1" ]; then
echo "stageCniPlugins=$v is documented as OFF but rendered $n init" >&2
echo "containers (expected 1: install-multus-binary alone)." >&2
return 1
fi
done
# And a spelling outside the documented set must stop the render rather than
# resolving to either direction.
if helm template --set-string stageCniPlugins=maybe packages/system/multus >/dev/null 2>&1; then
echo "stageCniPlugins=maybe rendered instead of failing the template" >&2
return 1
fi
}
@test "every line the patch owns still matches the committed template" {
# customize-deployment.patch is the SOURCE: `make update` does
# `rm -rf templates && wget && patch`, so anything present only in the template
# is discarded on the next run. Reverse-applying the whole patch proves every
# line the PATCH OWNS still matches; it says nothing about the rest of the file.
#
# --fuzz=0 because `patch` defaults to a fuzz factor of 2 and will drop context
# lines to make a hunk fit.
d=$(mktemp -d)
mkdir -p "$d/templates"
sed 's|^\( *\)image: .*multus-cni.*|\1image: ghcr.io/k8snetworkplumbingwg/multus-cni:snapshot-thick|' \
"$TEMPLATE" > "$d/templates/multus-daemonset-thick.yml"
# Non-empty, or a moved template turns this into a patch against nothing.
if [ ! -s "$d/templates/multus-daemonset-thick.yml" ]; then
echo "the template is empty or missing; nothing to compare the patch to" >&2
rm -rf "$d"
return 1
fi
# --fuzz=0 for the same reason: a dropped context line is an edit absorbed
# rather than reported.
if ! (cd "$d" && patch -R --dry-run --fuzz=0 -p4 --input="$PATCHFILE" >"$d/out" 2>&1); then
echo "customize-deployment.patch no longer describes the committed template." >&2
echo "The patch is the source: anything present only in the template is" >&2
echo "discarded by the next 'make update', and anything present only in the" >&2
echo "patch appears on every node at that point without ever being tested." >&2
cat "$d/out" >&2
rm -rf "$d"
return 1
fi
rm -rf "$d"
}
@test "every multus-cni reference in the manifest is the same string" {
# Every container that must run the multus image, found BY NAME. Selecting the
# image fields by what they already contain finds only those that still say
# multus-cni, so a container repointed at another image leaves the comparison
# instead of failing it.
rendered=$(helm template --set stageCniPlugins=true packages/system/multus)
for c in install-multus-binary install-cni-plugins kube-multus; do
img=$(printf '%s' "$rendered" | yq eval "select(.kind == \"DaemonSet\") | [.spec.template.spec.initContainers[], .spec.template.spec.containers[]] | .[] | select(.name == \"$c\") | .image" -)
case "$img" in
ghcr.io/cozystack/cozystack/multus-cni:*) ;;
*)
echo "container $c runs '$img', not the cozystack multus image." >&2
echo "All three run the same image by construction: the package's" >&2
echo "image: target rewrites every multus-cni line together." >&2
return 1 ;;
esac
done
# `make image` sed's the built reference over every multus-cni line, so they
# are one string by construction. Asserted as identity rather than per-index so
# a fourth container is covered without touching this test.
#
# `image:[[:space:]]+`, not a literal space: YAML permits any run, and a
# hard-coded one makes the pattern match nothing rather than match differently.
refs=$(grep -oE 'image:[[:space:]]+[^[:space:]]*multus-cni[^[:space:]]*' "$TEMPLATE" |
sed 's/^image:[[:space:]]*//' | sort -u)
count=$(printf '%s\n' "$refs" | grep -c . || true)
if [ "$count" = "0" ]; then
echo "no multus-cni image reference found in the template at all; if the" >&2
echo "image was renamed, this test and the package's image: target both" >&2
echo "need updating together." >&2
return 1
fi
if [ "$count" != "1" ]; then
echo "the manifest carries $count different multus-cni references:" >&2
printf '%s\n' "$refs" >&2
echo "They must be one string. make image writes all of them together, so a" >&2
echo "difference here means one was edited by hand." >&2
return 1
fi
}