mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 07:37:36 +00:00
master moved deb-install's container helpers into testing/lib/systemd-scenario.sh, so maint's changes to them are applied there: build_image passes extra arguments to docker build, the container carries the CI run label, and wait_for_systemd uses the bounded systemd_state. rpm-install shares that file and gains the same. deb-install keeps only its own remove_image and check_holds_package. check-image-scoping.sh takes one list of run-scoped tag prefixes for both lines' install suites. runner.py keeps master's text, which maint's settle port already matches.
261 lines
13 KiB
Bash
Executable File
261 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# ── Test-image and build-context scoping guard ──────────────────────────────
|
|
# A local CI run builds fips-test:<run-id> from a build context of its own and
|
|
# hands both to every suite, through FIPS_TEST_IMAGE / FIPS_TEST_APP_IMAGE and
|
|
# FIPS_BUILD_CONTEXT. It does NOT write fips-test:latest and does not build from
|
|
# the shared testing/docker directory, deliberately.
|
|
#
|
|
# Both halves protect the same thing. While a bridge back to the shared mutable
|
|
# tag existed, a consumer that named it directly kept working while resolving
|
|
# whichever concurrent run wrote the tag last. The shared build context is that
|
|
# same failure one level down: testing/docker/ is a single directory in the
|
|
# working tree, so two runs racing on its CONTENTS produce a correctly
|
|
# per-run-tagged image built from the other run's binaries — scoping the tag
|
|
# alone does not close it. Either way the verdict is recorded against a commit
|
|
# whose binaries had not run, and nothing in the harness compares a running
|
|
# container's binary against the commit under test, so that failure is silent
|
|
# and leaves no artifact.
|
|
#
|
|
# The bridge is gone, so a consumer that names the shared tag now fails loudly
|
|
# at run time. This guard is the static half: it stops one being reintroduced,
|
|
# because the reintroduction is invisible on any host where a hand build has
|
|
# left an fips-test:latest and a populated testing/docker/ lying around.
|
|
#
|
|
# The install suites have the same failure in images of their own. Each bakes
|
|
# the run's package or binaries into an image it builds itself, and a tag that
|
|
# is not scoped to the run is rebuilt by a concurrent run between this run's
|
|
# build and its boot, so this run tests the other run's artifact. That
|
|
# happened on the internal builder on 2026-10-07. Those tags carry the run's
|
|
# suffix, as the container names do.
|
|
#
|
|
# What counts as a violation:
|
|
# * a reference to the shared image name, whether tagged :latest or left
|
|
# untagged — docker resolves a bare `fips-test` to fips-test:latest, so
|
|
# `image: fips-test` reintroduces the identical defect;
|
|
# * a build context that names the shared testing/docker directory;
|
|
# * a tag with one of the run-scoped prefixes below that does not end in
|
|
# ${FIPS_CI_NAME_SUFFIX:-};
|
|
# unless it is a comment, the documented ${FIPS_TEST_IMAGE:-...} or
|
|
# ${FIPS_BUILD_CONTEXT:-...} fallback, or one of the individually justified
|
|
# references listed below.
|
|
#
|
|
# Exit 0 = clean. Exit 1 = an unexpected reference. Exit 2 = the guard could
|
|
# not run; never treated as a pass.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
# The shared image name: tagged :latest, or left untagged, which docker
|
|
# resolves to :latest. The run-scoped forms fips-test:${CI_RUN_ID} and the
|
|
# comparison harness's fips-test:compare-<ref> must NOT match, so the untagged
|
|
# half requires the name to end at a character that can continue neither a
|
|
# repository name nor a tag.
|
|
SHARED_IMAGE_RE="fips-test(-app)?:latest|fips-test(-app)?([^-:._[:alnum:]]|\$)"
|
|
|
|
# The shared build context: a path whose LAST component is the tracked
|
|
# testing/docker directory. A path that continues past it — ../docker/resolv.conf,
|
|
# "\$SCRIPT_DIR"/docker/* — names a file inside the directory rather than using
|
|
# the directory as a context, and is not what races. The per-run context is
|
|
# docker-<run-id>, so the terminator class also keeps that from matching.
|
|
SHARED_CONTEXT_RE="/docker([[:space:]\"'}]|\$)|^[[:space:]]*(build|context):[[:space:]]*[\"']?docker/?[\"']?[[:space:]]*\$"
|
|
|
|
# Tag prefixes, each a repository and the colon (and any fixed start of the tag
|
|
# after it), under which every tag bakes a run's artifact, so every such tag
|
|
# must be the run's own. A tag here is the prefix and everything after it up to
|
|
# the next quote or space; container names use a dash after the repository
|
|
# name and do not match. A prefix is read as an extended regular expression in
|
|
# the tag match, so keep it to letters, digits, '-' and ':'. The dns-resolver
|
|
# suite's other fips-dns-test tags hold no artifact of the run and are shared
|
|
# on purpose; only its e2e images hold the run's binaries.
|
|
RUN_SCOPED_TAGS=(
|
|
fips-rpm-test:
|
|
fips-deb-test:
|
|
fips-dns-test:e2e-
|
|
)
|
|
RUN_SUFFIX='${FIPS_CI_NAME_SUFFIX:-}'
|
|
|
|
# Individually justified references, as "<path under testing/>::<text the line
|
|
# must contain>". The exemption is scoped to the reference, not to the file: a
|
|
# NEW shared-tag or shared-context reference added anywhere in one of these
|
|
# files is still a violation.
|
|
#
|
|
# Two of these files ARE reached from ci-local.sh, which is why a
|
|
# whole-file exemption would not be honest here:
|
|
# * sidecar/scripts/test-sidecar.sh is invoked with --skip-build, so the
|
|
# build block holding its two references never executes under CI;
|
|
# * ci-cleanup.sh is invoked to reap, and its references only pick the
|
|
# throwaway image ip(8) runs in — it addresses no test container.
|
|
ALLOWED_REFS=(
|
|
# The developer build. The shared directory is its context and the shared
|
|
# tags are its product; that is the whole point of the script, and CI does
|
|
# not run it.
|
|
'scripts/build.sh::DOCKER_DIR="$TESTING_DIR/docker"'
|
|
'scripts/build.sh::docker build -t fips-test:latest "$DOCKER_DIR"'
|
|
'scripts/build.sh::docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app"'
|
|
'scripts/build.sh::echo "Done. Images: fips-test:latest, fips-test-app:latest"'
|
|
|
|
# The hand-run sidecar build, behind its --skip-build guard. ci-local.sh
|
|
# always passes that flag, so under CI these two lines do not run and the
|
|
# image comes from FIPS_TEST_IMAGE like everywhere else.
|
|
'sidecar/scripts/test-sidecar.sh::docker build -t fips-test:latest "$DOCKER_DIR"'
|
|
'sidecar/scripts/test-sidecar.sh::docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app"'
|
|
|
|
# Hand-run A/B between two refs: it retags whatever the developer build
|
|
# last produced, under a name of its own.
|
|
'static/scripts/iperf-compare-refs.sh::docker tag fips-test:latest "$tag"'
|
|
|
|
# The image ip(8) runs in during a reap, wanted only for its iproute2. The
|
|
# repository-wide listing and the literal tag are the last two fallbacks,
|
|
# after --veth-image and the run's own FIPS_TEST_IMAGE have both come back
|
|
# empty; neither addresses a container under test.
|
|
'ci-cleanup.sh::docker image ls --format'
|
|
'ci-cleanup.sh::VETH_IMAGE="fips-test:latest"'
|
|
|
|
# Per-ref interop images. This copies three files OUT of the shared
|
|
# directory into a context of its own and builds from that, so it reads the
|
|
# shared directory but never uses it as a context.
|
|
'interop/build-images.sh::DOCKER_CTX_SRC="$REPO_ROOT/testing/docker"'
|
|
|
|
# The veth self-test's stand-in docker client: a script it writes into its
|
|
# scratch directory and puts first on PATH. It is a file named docker, not
|
|
# a build context, and no image is built or run.
|
|
'chaos/veth-host-test.sh::SHIM="$WORK/bin/docker"'
|
|
)
|
|
|
|
# This guard is the one file that must contain the patterns it looks for, so it
|
|
# is swept out of its own file list. The exemption is bounded rather than
|
|
# trusted: the guard must not be able to hide a real invocation behind it, so
|
|
# assert it runs no container command. That catches one written as its own
|
|
# statement, which is the shape a reintroduction here would take; it would not
|
|
# catch one buried inside a command substitution.
|
|
GUARD_REL="check-image-scoping.sh"
|
|
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
echo "check-image-scoping: git not available, cannot sweep" >&2
|
|
exit 2
|
|
fi
|
|
if [[ ! -d "$SCRIPT_DIR" ]]; then
|
|
echo "check-image-scoping: $SCRIPT_DIR missing" >&2
|
|
exit 2
|
|
fi
|
|
if [[ ! -f "$SCRIPT_DIR/$GUARD_REL" ]]; then
|
|
echo "check-image-scoping: cannot find itself at $SCRIPT_DIR/$GUARD_REL" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Tracked files only, and no documentation: prose naming the tag or the
|
|
# directory is describing it, not resolving it.
|
|
if ! tracked="$(git -C "$SCRIPT_DIR/.." ls-files -- testing/)"; then
|
|
echo "check-image-scoping: git ls-files failed, refusing to pass" >&2
|
|
exit 2
|
|
fi
|
|
if [[ -z "$tracked" ]]; then
|
|
echo "check-image-scoping: no tracked files under testing/, refusing to pass" >&2
|
|
exit 2
|
|
fi
|
|
mapfile -t files < <(printf '%s\n' "$tracked" | grep -vE '\.md$')
|
|
if [[ ${#files[@]} -eq 0 ]]; then
|
|
echo "check-image-scoping: no tracked non-doc files under testing/, refusing to pass" >&2
|
|
exit 2
|
|
fi
|
|
|
|
violations=0
|
|
declare -A tags_seen=()
|
|
|
|
if grep -qE '^[[:space:]]*docker[[:space:]]' "$SCRIPT_DIR/$GUARD_REL"; then
|
|
echo "FAIL testing/$GUARD_REL runs a docker command."
|
|
echo " It is exempt from its own sweep only because it resolves no image."
|
|
violations=$((violations + 1))
|
|
fi
|
|
|
|
# True when this file:line is one of the justified references above.
|
|
allowed_ref() {
|
|
local rel="$1" text="$2" entry path frag
|
|
for entry in "${ALLOWED_REFS[@]}"; do
|
|
path="${entry%%::*}"
|
|
frag="${entry#*::}"
|
|
[[ "$rel" == "$path" && "$text" == *"$frag"* ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
for f in "${files[@]}"; do
|
|
rel="${f#testing/}"
|
|
[[ "$rel" == "$GUARD_REL" ]] && continue
|
|
[[ -f "$SCRIPT_DIR/../$f" ]] || continue
|
|
|
|
while IFS= read -r hit; do
|
|
n="${hit%%:*}"
|
|
text="${hit#*:}"
|
|
# A comment line is describing the tag, not resolving it. Shell, python
|
|
# and yaml all use #; nothing under testing/ uses // for comments.
|
|
[[ "$text" =~ ^[[:space:]]*# ]] && continue
|
|
# The documented indirection: the shared tag as a FALLBACK, which is
|
|
# what a bare hand run is supposed to get.
|
|
[[ "$text" == *'${FIPS_TEST_IMAGE:-fips-test:latest}'* ]] && continue
|
|
[[ "$text" == *'${FIPS_TEST_APP_IMAGE:-fips-test-app:latest}'* ]] && continue
|
|
[[ "$text" == *'os.environ.get("FIPS_TEST_IMAGE", "fips-test:latest")'* ]] && continue
|
|
allowed_ref "$rel" "$text" && continue
|
|
echo "FAIL $f:$n names the shared test image directly:"
|
|
echo " $text"
|
|
violations=$((violations + 1))
|
|
done < <(grep -nE "$SHARED_IMAGE_RE" "$SCRIPT_DIR/../$f" 2>/dev/null)
|
|
|
|
while IFS= read -r hit; do
|
|
n="${hit%%:*}"
|
|
text="${hit#*:}"
|
|
[[ "$text" =~ ^[[:space:]]*# ]] && continue
|
|
# The same indirection for the context: the shared directory is allowed
|
|
# only as the fallback a bare hand run gets. Compose resolves a relative
|
|
# build.context against the compose FILE's directory, which is why the
|
|
# harness exports an absolute path here.
|
|
[[ "$text" == *'${FIPS_BUILD_CONTEXT:-'* ]] && continue
|
|
allowed_ref "$rel" "$text" && continue
|
|
echo "FAIL $f:$n builds from the shared testing/docker context:"
|
|
echo " $text"
|
|
violations=$((violations + 1))
|
|
done < <(grep -nE "$SHARED_CONTEXT_RE" "$SCRIPT_DIR/../$f" 2>/dev/null)
|
|
|
|
for prefix in "${RUN_SCOPED_TAGS[@]}"; do
|
|
while IFS= read -r hit; do
|
|
n="${hit%%:*}"
|
|
text="${hit#*:}"
|
|
[[ "$text" =~ ^[[:space:]]*# ]] && continue
|
|
while IFS= read -r tag; do
|
|
tags_seen[$prefix]=$(( ${tags_seen[$prefix]:-0} + 1 ))
|
|
[[ "$tag" == *"$RUN_SUFFIX" ]] && continue
|
|
echo "FAIL $f:$n tags $prefix without the run suffix:"
|
|
echo " $text"
|
|
violations=$((violations + 1))
|
|
done < <(grep -oE -- "${prefix}[^\"'[:space:]]*" <<<"$text")
|
|
done < <(grep -nF -- "$prefix" "$SCRIPT_DIR/../$f" 2>/dev/null)
|
|
done
|
|
done
|
|
|
|
# A prefix with no tag found at all means the sweep looked at nothing for it,
|
|
# most likely because the harness renamed the tag, not that every tag is scoped.
|
|
for prefix in "${RUN_SCOPED_TAGS[@]}"; do
|
|
if [[ -z "${tags_seen[$prefix]:-}" ]]; then
|
|
echo "check-image-scoping: no tag starting $prefix found under testing/; if the tag" >&2
|
|
echo "was renamed or removed, update RUN_SCOPED_TAGS, refusing to pass" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
|
|
if [[ $violations -gt 0 ]]; then
|
|
echo ""
|
|
echo "check-image-scoping: $violations unscoped reference(s) to the shared test image,"
|
|
echo "the shared build context, or a run-scoped tag."
|
|
echo "Read FIPS_TEST_IMAGE (default \${FIPS_TEST_IMAGE:-fips-test:latest}) for the image"
|
|
echo "and FIPS_BUILD_CONTEXT (default \${FIPS_BUILD_CONTEXT:-../docker}) for the context,"
|
|
echo "and end a tag starting ${RUN_SCOPED_TAGS[*]} with $RUN_SUFFIX."
|
|
echo "A run that resolves either shared name can execute a concurrent run's binaries"
|
|
echo "and record the verdict against this commit."
|
|
exit 1
|
|
fi
|
|
|
|
echo "check-image-scoping: no unscoped references to the shared test image, build context"
|
|
echo "or run-scoped tags (${RUN_SCOPED_TAGS[*]})"
|
|
exit 0
|