Files
fips/testing/openwrt/maintainer-scripts-test.sh
Johnathan Corgan 26abd1be67 Give every CI container the run label and make the label check require it
The startup-stop scenarios in the gateway suite, the OpenWrt maintainer
script scenarios and the Debian package build and depends check started
containers with no CI label at all. ci-cleanup.sh selects containers by
label or compose project, never by name, so a container from one of these
sites that outlived a killed run was reaped by nothing: not the run's own
teardown, and not the builder's broad sweep.

Each site now carries the generic and run labels, through CI_LABELS where
the script sources testing/lib/image-build.sh and as the literal pair in
startup-stop-test.sh, which sources nothing. Outside local CI the run label
reads "manual", which no teardown reaps by run.

ci-cleanup.sh's two ip(8) helpers stay unlabelled on purpose: the builder
skips its orphan sweep while any generic-labelled container is up, so a
labelled helper would let one run's teardown hold off that sweep. Each is
marked with a "# ci-labels:" comment on the command's last line, ready for
the label check to read as an exemption; on the first line it would follow
the continuation backslash and end the command there.

check-ci-labels.py failed a docker command that gave the generic CI label
without the run label, but passed one that carried no label at all, so a
new container site written without labels passed every repository check.
Such a container is worse off than a generic-labelled one: ci-cleanup.sh
selects by label or compose project, never by name, so nothing reaps it.

Rule 3 now fails a docker run, create, container run, container create,
network create or volume create under testing/ or packaging/ that carries
no run label, unless rule 1 already reported the line. It reads shell
words, so a quoted error message naming "docker network create" is not a
command, while a command after a separator, behind timeout or "if !", or
inside a $(...) or backtick substitution, quoted or not, is still found.
Builds stay out: shared images keep fixed tags and no labels by design.

A container that must stay unlabelled takes a trailing "# ci-labels:
<reason>" comment. Rule 4 fails one with no reason, and one placed after a
continuation backslash, where the comment would end the command on that
line and run the rest as a separate command.

File arguments are now scoped by their path relative to the work tree, so
testing an uncommitted copy reads it as the sweep would. Docker's short -l
flag is read as a label, and the self-test carries a red or a green for
each new shape.
2026-10-25 09:15:02 +00:00

174 lines
8.0 KiB
Bash
Executable File

#!/bin/bash
# ── OpenWrt maintainer-script scenarios ─────────────────────────────────────
# Runs testing/openwrt/scenarios.sh inside a busybox container, so the package
# scripts and the fips-gateway init script are interpreted by ash rather than
# by the host's bash or dash. The scripts ship to routers and are only ever run
# under ash there; a construct bash accepts and ash does not would otherwise
# surface on a router.
#
# Then runs testing/openwrt/firewall-scenarios.sh in four pinned OpenWrt
# rootfs images, which carry the real uci, fw4 and nft, so 90-fips-setup's
# firewall zone and upgrade migration, and fips-mesh-setup's mesh zone, run
# against the firewall they configure.
# Then runs testing/openwrt/init-scenarios.sh in the same four images, where
# the fips-gateway init script runs under the image's own rc.common, procd.sh,
# flock and uci, against a stub gateway, with no network.
# The firewall containers get NET_ADMIN so fw4 can load its table, into the
# container's own network namespace; they have no network otherwise. Each also
# gets a small tmpfs, where the cases that save onto a full overlay put
# /etc/config.
#
# No FIPS binary and no shared test image are used.
#
# Exit 0 = every scenario passed. Exit 1 = at least one failed. Exit 2 = the
# harness could not run; never treated as a pass. The firewall runs go ahead
# when the busybox run failed, and the worst code of all the runs is returned.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# Pinned by digest so the shell under test does not change under a run: the
# 1.37 tag moves with every 1.37.x rebuild. This is the multi-arch index digest
# of busybox:1.37.0 as of 2026-10-01. Bump it deliberately, reading the new
# digest with `docker buildx imagetools inspect busybox:<version>`.
# Overridable for trying another ash build.
IMAGE="${OPENWRT_ASH_IMAGE:-busybox:1.37.0@sha256:bdf57e528e45e4433820e045b29b4597825a1c9e38353532d90a01445013f82e}"
# The OpenWrt releases the package supports, each pinned by digest as of
# 2026-10-10 and read the same way (25.12.5 is published as a single manifest,
# the others as multi-arch indexes). The two older releases run a subset of the
# cases to keep the job's time: an upgrade, a fresh install, port-forward
# scoping, a failed reload and its recovery, an unloaded firewall, and a save
# onto a full overlay, which matters most on 22.03, whose default input policy
# is ACCEPT; and for the mesh zone an add, a two-radio remove, a firewall
# reload during a remove, a failed reload, an upgrade, and a failed reload
# during an upgrade.
FW_OLDER_CASES="fw_upgrade_moves_fips0_into_its_own_zone fw_fresh_install_creates_live_zone fw_operator_scoping_renders_before_port_forwards fw_failed_reload_blocks_new_fips0_connections fw_recovery_after_failed_reload_clears_block fw_unloaded_firewall_is_reported fw_full_overlay_save_blocks_and_keeps_setup mesh_add_puts_interface_in_reject_zone mesh_two_radios_share_one_zone_and_remove_cleans_up mesh_remove_keeps_zone_until_interface_is_gone mesh_failed_reload_drops_mesh_input mesh_upgrade_zones_existing_interfaces mesh_hook_failed_reload_drops_mesh_input"
FW_IMAGES=(
"openwrt/rootfs:x86-64-25.12.5@sha256:c5d5f05bab4ce06a4e840b3573671a06ec8ae9842273188ffc53f7021836b8e2|"
"openwrt/rootfs:x86-64-24.10.8@sha256:9972a4b4747cd136abd597475d7b88c51a49fd849d0d53f069a2f4bf446061b9|"
"openwrt/rootfs:x86-64-23.05.6@sha256:f53afb386d05651ada9a4a964e0779930f30962a2abc2fadf1b03bfcbcbabb14|$FW_OLDER_CASES"
"openwrt/rootfs:x86-64-22.03.7@sha256:f6e5b98399ae4ff89fc1a2de2da5b94c520c8f82aa1e5c717fb190da8b3313ed|$FW_OLDER_CASES"
)
if ! command -v docker >/dev/null 2>&1; then
echo "openwrt-scripts: docker not found; cannot run the ash scenarios" >&2
exit 2
fi
for f in scenarios.sh firewall-scenarios.sh init-scenarios.sh; do
if [[ ! -f "$SCRIPT_DIR/$f" ]]; then
echo "openwrt-scripts: missing $SCRIPT_DIR/$f" >&2
exit 2
fi
done
# Each image is pulled once, with a few attempts, so a registry outage reads as
# the harness not running rather than as a failed case.
pull() {
local attempt
for attempt in 1 2 3; do
docker pull -q "$1" >/dev/null && return 0
echo "openwrt-scripts: pulling $1 failed (attempt $attempt)" >&2
[[ $attempt -lt 3 ]] && sleep 5
done
return 1
}
pull_started=$SECONDS
for spec in "$IMAGE" "${FW_IMAGES[@]}"; do
image="${spec%%|*}"
if ! pull "$image"; then
echo "openwrt-scripts: could not pull $image" >&2
exit 2
fi
done
echo "openwrt-scripts: images ready in $((SECONDS - pull_started))s"
# The .apk wraps the shared bodies for its upgrade path. package-test.sh builds
# the package on the host with the real build-apk.sh, checks what it registers,
# and leaves the four scripts here so the scenarios run exactly what ships.
# The directory is bind-mounted into the container, so it is made by
# shared_tmpdir under the checkout, not /tmp; testing/lib/image-build.sh says
# why. --sweep clears what a killed run left behind.
# shellcheck source=SCRIPTDIR/../lib/image-build.sh
. "$SCRIPT_DIR/../lib/image-build.sh"
APK_DIR="$(shared_tmpdir --sweep "$PROJECT_ROOT/target" openwrt-apk)" || {
echo "openwrt-scripts: could not create a temporary directory under $PROJECT_ROOT/target" >&2
exit 2
}
trap 'rm -rf "$APK_DIR"' EXIT
bash "$SCRIPT_DIR/package-test.sh" --keep "$APK_DIR"
rc=$?
if [[ $rc -ne 0 ]]; then
echo "openwrt-scripts: package-test.sh exited $rc" >&2
exit $rc
fi
docker run --rm --network none "${CI_LABELS[@]}" \
-v "$PROJECT_ROOT:/src:ro" \
-v "$APK_DIR:/apk:ro" \
-e REPO=/src \
-e APK_SCRIPTS=/apk \
-e "POSTINST=${POSTINST:-}" \
-e "PRERM=${PRERM:-}" \
-e "PREINST=${PREINST:-}" \
-e "INIT_GATEWAY=${INIT_GATEWAY:-}" \
"$IMAGE" sh /src/testing/openwrt/scenarios.sh
rc=$?
if [[ $rc -ne 0 && $rc -ne 1 ]]; then
echo "openwrt-scripts: the container exited $rc, so the scenarios did not report" >&2
rc=2
fi
worst=$rc
# FW_SETUP, FW_MESH_SETUP and FW_SNIPPETS pass through from the caller when
# set, to run the firewall scenarios against another setup script or mesh
# helper, or without the snippets.
for spec in "${FW_IMAGES[@]}"; do
image="${spec%%|*}"
cases="${spec#*|}"
fw_env=(-e FW_SETUP -e FW_MESH_SETUP -e FW_SNIPPETS)
if [[ -n "$cases" ]]; then
fw_env+=(-e "FW_CASES=$cases")
else
fw_env+=(-e FW_CASES)
fi
echo "==> firewall scenarios in $image"
docker run --rm --network none --cap-add NET_ADMIN "${CI_LABELS[@]}" \
--tmpfs /fw-small:size=128k \
-v "$PROJECT_ROOT:/src:ro" \
-v "$APK_DIR:/apk:ro" \
"${fw_env[@]}" \
"$image" sh /src/testing/openwrt/firewall-scenarios.sh
rc=$?
if [[ $rc -ne 0 && $rc -ne 1 ]]; then
echo "openwrt-scripts: the $image container exited $rc, so its firewall scenarios did not report" >&2
rc=2
fi
[[ $rc -gt $worst ]] && worst=$rc
done
# INIT_GATEWAY and PRERM pass through from the caller when set, as for
# scenarios.sh; inside the container they name paths under /src or /apk.
for spec in "${FW_IMAGES[@]}"; do
image="${spec%%|*}"
echo "==> init script scenarios in $image"
docker run --rm --network none "${CI_LABELS[@]}" \
-v "$PROJECT_ROOT:/src:ro" \
-v "$APK_DIR:/apk:ro" \
-e INIT_GATEWAY -e PRERM -e INIT_CASES \
"$image" sh /src/testing/openwrt/init-scenarios.sh
rc=$?
if [[ $rc -ne 0 && $rc -ne 1 ]]; then
echo "openwrt-scripts: the $image container exited $rc, so its init script scenarios did not report" >&2
rc=2
fi
[[ $rc -gt $worst ]] && worst=$rc
done
echo "openwrt-scripts: finished in ${SECONDS}s, exit $worst"
exit $worst