mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Replace the previous OR-based assertion in scripts/tests/environment (which trivially passed because the Nexus log always grows from health probes) with a richer set of helpers split across two files under utils/ for SRP/SPOT. scripts/tests/environment/utils/cache.sh (new): * expected_system_proxy_for_runtime maps DEV_RUNTIME_IMAGE to the Nexus proxy that handles its system package manager (apt-debian, apt-ubuntu, yum-fedora, yum-rocky; empty for Manjaro/Arch where pacman is not cached). * nexus_repo_requests counts only /repository/<name>/... entries in the Nexus request log, optionally scoped to one proxy. Filters out health probes, admin REST calls, and Web-UI traffic. * registry_cache_flows counts every cache-flow status (HIT, MISS, REVALIDATED, UPDATING, STALE) so cold deploys also count. * assert_caches_used fails with a per-cache breakdown when any of three independent conditions is violated: total Nexus repo traffic = 0, the runtime's expected system proxy has 0 cached requests, or the deploy itself produced 0 registry-cache flows. * probe_caches actively fetches a known upstream resource through each cache (apt-debian/dists/bookworm/Release for the package cache, /ca.crt for the registry cache). * probe_did_inner_build drives a throwaway docker build whose apt-get update runs against deb.debian.org. The build container is placed on the compose network and pinned via --add-host to the frontend IP, exactly like the runtime override that roles/sys-svc-compose/files/compose.py injects for app builds. After the build the test snapshots ``docker logs infinito-package-cache-frontend`` and asserts the frontend recorded new requests, proving DiD apt traffic flows through nginx -> Nexus apt-debian and not directly to upstream. DOCKER_BUILDKIT=0 because BuildKit refuses --network=<compose-net>. scripts/tests/environment/utils/common.sh (renamed from lib.sh): keeps the bootstrap, constants, inventory inspect, and assert_http_status; cache helpers moved out so the file is single-concern. scripts/tests/environment/07_deploy_minimal.sh sources both files; order is snapshot, deploy, probe_caches, probe_did_inner_build, snapshot, assert_caches_used. Active probes contribute traffic that the AND assertion verifies, so the assertion is robust on deploys that do not naturally trigger inner-build apt traffic. .github/workflows/test-environment.yml: propagate DEV_RUNTIME_IMAGE into the inner ``docker run`` so per-distro probe target and proxy expectation resolve correctly inside the orchestrator. .gitignore + .dockerignore: also ignore .claude/settings.local.json, which compose mounts as /dev/null and which the classic docker builder would otherwise try (and fail) to read into the build context. Verified end-to-end with five iterations of ``make act-workflow ACT_WORKFLOW=.github/workflows/test-environment.yml ACT_JOB=test-environment ACT_MATRIX='dev_runtime_image:debian:bookworm'``. The final iteration reports +81 apt-debian, +235 registry-cache flows, +13 frontend log lines from the DiD probe, and "[OK] both cache stacks observed activity" plus "[OK] DiD inner-build flowed through cache frontend".
237 lines
9.3 KiB
Bash
Executable File
237 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Cache-stack assertions and probes for the environment test suite.
|
|
# Verifies that BOTH the registry-cache and the package-cache (Nexus +
|
|
# nginx frontend) actually serve traffic during a deploy, including
|
|
# the DiD inner-build path that the runner-side traffic alone does
|
|
# NOT exercise.
|
|
#
|
|
# This file is sourced ON TOP OF common.sh by callers that need cache
|
|
# checks (currently only 07_deploy_minimal.sh).
|
|
set -euo pipefail
|
|
|
|
# Map runtime image to the Nexus proxy repo that proxies its system
|
|
# package manager. Empty string means no system-level proxy is wired
|
|
# for that runtime (e.g. Arch / Manjaro use pacman, which is not
|
|
# cached). Echoes the proxy name on stdout.
|
|
expected_system_proxy_for_runtime() {
|
|
local image="${DEV_RUNTIME_IMAGE:-}"
|
|
case "${image}" in
|
|
*debian*) echo "apt-debian" ;;
|
|
*ubuntu*) echo "apt-ubuntu" ;;
|
|
*fedora*) echo "yum-fedora" ;;
|
|
*centos* | *rocky*) echo "yum-rocky" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
# Count Nexus request-log entries for a given proxy repo, or for any
|
|
# repo when the argument is empty. Filters out health-probe, admin,
|
|
# and UI traffic so the count reflects real pull-through activity.
|
|
nexus_repo_requests() {
|
|
local repo="${1:-}"
|
|
local pattern
|
|
if [[ -n "${repo}" ]]; then
|
|
pattern="\"(GET|HEAD|POST|PUT) /repository/${repo}/"
|
|
else
|
|
pattern="\"(GET|HEAD|POST|PUT) /repository/[^/ ]+/"
|
|
fi
|
|
docker exec infinito-package-cache sh -c \
|
|
"grep -cE '${pattern}' /nexus-data/log/request.log 2>/dev/null || echo 0" \
|
|
2>/dev/null | awk '{print $1+0}'
|
|
}
|
|
|
|
# Count registry-cache flow entries (HIT, MISS, REVALIDATED, UPDATING,
|
|
# STALE). Both HIT and MISS prove the cache layer is engaged; counting
|
|
# only HITs misses cold deploys where everything is a first pull.
|
|
registry_cache_flows() {
|
|
docker logs infinito-registry-cache 2>&1 |
|
|
grep -cE '"upstream_cache_status":"(HIT|MISS|REVALIDATED|UPDATING|STALE)"' ||
|
|
true
|
|
}
|
|
|
|
# Snapshot all cache counters relevant for the AND-assertion. Output
|
|
# is a single space-separated line consumed by ``assert_caches_used``.
|
|
# Order matters: total-nexus, expected-distro-proxy, registry-flows.
|
|
cache_snapshot() {
|
|
local nexus_total proxy_count registry_flows expected_proxy
|
|
expected_proxy="$(expected_system_proxy_for_runtime)"
|
|
nexus_total="$(nexus_repo_requests "")"
|
|
proxy_count="$(nexus_repo_requests "${expected_proxy}")"
|
|
registry_flows="$(registry_cache_flows)"
|
|
printf '%s %s %s\n' \
|
|
"${nexus_total:-0}" "${proxy_count:-0}" "${registry_flows:-0}"
|
|
}
|
|
|
|
# Assert that BOTH cache stacks were engaged during the deploy. Fails
|
|
# loudly if either stack is silent and reports a per-cache breakdown
|
|
# so the operator can see which side regressed.
|
|
#
|
|
# Usage: assert_caches_used <before> <after>
|
|
# where <before> and <after> are the strings returned by
|
|
# ``cache_snapshot`` (3 fields each).
|
|
assert_caches_used() {
|
|
local before="${1}" after="${2}"
|
|
local nexus_total_before proxy_before registry_before
|
|
local nexus_total_after proxy_after registry_after
|
|
read -r nexus_total_before proxy_before registry_before <<<"${before}"
|
|
read -r nexus_total_after proxy_after registry_after <<<"${after}"
|
|
|
|
local delta_nexus_total=$((nexus_total_after - nexus_total_before))
|
|
local delta_proxy=$((proxy_after - proxy_before))
|
|
local delta_registry=$((registry_after - registry_before))
|
|
local expected_proxy
|
|
expected_proxy="$(expected_system_proxy_for_runtime)"
|
|
|
|
echo "[cache] package proxy requests (any repo) +${delta_nexus_total}" \
|
|
"(now ${nexus_total_after})"
|
|
if [[ -n "${expected_proxy}" ]]; then
|
|
echo "[cache] package proxy '${expected_proxy}' +${delta_proxy}" \
|
|
"(now ${proxy_after})"
|
|
else
|
|
echo "[cache] no Nexus system-proxy mapped for" \
|
|
"DEV_RUNTIME_IMAGE='${DEV_RUNTIME_IMAGE:-?}'; skipping per-proxy check"
|
|
fi
|
|
echo "[cache] registry-cache flows (HIT|MISS|...) +${delta_registry}" \
|
|
"(now ${registry_after})"
|
|
|
|
local -a failures=()
|
|
|
|
# Package-cache assertion: cumulative proxy traffic must exist for
|
|
# the runtime's expected system-package proxy. Cumulative (not
|
|
# delta) because most apt/yum traffic happens during 01_install,
|
|
# well before this assertion fires.
|
|
if [[ "${nexus_total_after}" -le 0 ]]; then
|
|
failures+=("package-cache: no /repository/<name>/ traffic ever observed (Nexus seems unused)")
|
|
fi
|
|
if [[ -n "${expected_proxy}" && "${proxy_after}" -le 0 ]]; then
|
|
failures+=("package-cache: expected proxy '${expected_proxy}' for ${DEV_RUNTIME_IMAGE} has no cached requests")
|
|
fi
|
|
|
|
# Registry-cache assertion: the deploy itself must produce flow
|
|
# entries (HIT or MISS), since image pulls are part of the
|
|
# deploy.
|
|
if [[ "${delta_registry}" -le 0 ]]; then
|
|
failures+=("registry-cache: no HIT/MISS recorded during this deploy")
|
|
fi
|
|
|
|
if ((${#failures[@]} > 0)); then
|
|
echo "[FAIL] cache assertions:" >&2
|
|
printf ' - %s\n' "${failures[@]}" >&2
|
|
exit 1
|
|
fi
|
|
echo "[OK] both cache stacks observed activity (registry + package-cache, including '${expected_proxy:-N/A}')"
|
|
}
|
|
|
|
# Map runtime image to a (proxy, upstream-path) pair that the active
|
|
# probe can fetch through Nexus. The path picks a small, reliably-
|
|
# available index file on the upstream so the probe stays cheap.
|
|
_probe_target_for_runtime() {
|
|
local image="${DEV_RUNTIME_IMAGE:-}"
|
|
case "${image}" in
|
|
*debian*) echo "apt-debian dists/bookworm/Release" ;;
|
|
*ubuntu*) echo "apt-ubuntu dists/jammy/Release" ;;
|
|
*fedora*) echo "yum-fedora releases/40/Everything/x86_64/os/repodata/repomd.xml" ;;
|
|
*centos* | *rocky*) echo "yum-rocky 9/BaseOS/x86_64/os/repodata/repomd.xml" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
# Active end-to-end probe: hit each cache through its public surface
|
|
# and verify the request reaches the cache and is logged as proxy
|
|
# traffic. Catches regressions where the cache stack is up but stops
|
|
# proxying (mis-routed nginx, dead upstream, broken auth, expired CA,
|
|
# etc.). Calls ``exit 1`` if either probe fails.
|
|
probe_caches() {
|
|
local probe_target proxy_repo upstream_path
|
|
probe_target="$(_probe_target_for_runtime)"
|
|
if [[ -z "${probe_target}" ]]; then
|
|
echo "[probe] no probe target mapped for" \
|
|
"DEV_RUNTIME_IMAGE='${DEV_RUNTIME_IMAGE:-?}'; skipping package probe"
|
|
else
|
|
read -r proxy_repo upstream_path <<<"${probe_target}"
|
|
echo "[probe] package-cache: GET /repository/${proxy_repo}/${upstream_path}"
|
|
if ! docker exec infinito-package-cache curl \
|
|
--silent --fail --show-error --max-time 60 \
|
|
--output /dev/null \
|
|
"http://localhost:8081/repository/${proxy_repo}/${upstream_path}"; then
|
|
echo "[FAIL] package-cache: proxy '${proxy_repo}' did not serve" \
|
|
"'${upstream_path}'. The cache stack is up but pull-through is broken." >&2
|
|
exit 1
|
|
fi
|
|
echo "[OK] package-cache: '${proxy_repo}' served '${upstream_path}'"
|
|
fi
|
|
|
|
# Registry-cache (rpardini/docker-registry-proxy) is a transparent
|
|
# CONNECT proxy used by dockerd. It does not expose /v2/ directly;
|
|
# its only public HTTP endpoint is /ca.crt. A 200 there proves the
|
|
# cache container is alive and reachable via the proxy port. The
|
|
# AND-assertion against ``delta_registry > 0`` then confirms the
|
|
# deploy actually flowed through it.
|
|
echo "[probe] registry-cache: GET /ca.crt"
|
|
if ! docker exec infinito-registry-cache wget \
|
|
--quiet --tries=1 --timeout=10 \
|
|
--output-document=/dev/null \
|
|
"http://127.0.0.1:3128/ca.crt"; then
|
|
echo "[FAIL] registry-cache: /ca.crt did not respond. The cache" \
|
|
"container is unhealthy." >&2
|
|
exit 1
|
|
fi
|
|
echo "[OK] registry-cache: /ca.crt responded"
|
|
}
|
|
|
|
# Active DiD probe: drive a throwaway ``docker build`` whose
|
|
# ``apt-get update`` MUST flow through the package-cache frontend
|
|
# (``deb.debian.org`` is hijacked via ``--add-host`` to the frontend
|
|
# IP, exactly mirroring the runtime override that
|
|
# ``roles/sys-svc-compose/files/compose.py`` injects for app builds).
|
|
# Verifies the inner-build / DiD cache path, which the
|
|
# Ansible-driven runner-side traffic alone does NOT exercise.
|
|
probe_did_inner_build() {
|
|
local frontend_ip
|
|
frontend_ip="${INFINITO_PACKAGE_CACHE_FRONTEND_IP:-}"
|
|
if [[ -z "${frontend_ip}" ]]; then
|
|
echo "[probe-did] INFINITO_PACKAGE_CACHE_FRONTEND_IP unset; skipping DiD probe"
|
|
return 0
|
|
fi
|
|
|
|
local network="${COMPOSE_NETWORK:-infinito-nexus_default}"
|
|
local before after delta tmpdir tag rc=0
|
|
before="$(docker logs infinito-package-cache-frontend 2>&1 | wc -l)"
|
|
|
|
tmpdir="$(mktemp -d)"
|
|
cat >"${tmpdir}/Dockerfile" <<-'EOF'
|
|
FROM debian:bookworm-slim
|
|
RUN apt-get update -o Acquire::http::No-Cache=true
|
|
EOF
|
|
tag="cache-did-probe-$$"
|
|
|
|
echo "[probe-did] docker build --add-host deb.debian.org:${frontend_ip}" \
|
|
"--network ${network} (apt-get update via frontend)"
|
|
# Force the classic builder: BuildKit rejects user-defined compose
|
|
# networks ("not supported by buildkit"). The classic builder
|
|
# accepts arbitrary `--network <name>` so we can put the build on
|
|
# the same network as the frontend container.
|
|
if ! DOCKER_BUILDKIT=0 docker build --no-cache \
|
|
--network "${network}" \
|
|
--add-host "deb.debian.org:${frontend_ip}" \
|
|
-t "${tag}" "${tmpdir}" 2>&1 | tail -10; then
|
|
rc=1
|
|
fi
|
|
rm -rf "${tmpdir}"
|
|
docker rmi "${tag}" >/dev/null 2>&1 || true
|
|
|
|
if [[ "${rc}" -ne 0 ]]; then
|
|
echo "[FAIL] DiD probe: docker build failed (frontend, network or upstream broken)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
after="$(docker logs infinito-package-cache-frontend 2>&1 | wc -l)"
|
|
delta=$((after - before))
|
|
if [[ "${delta}" -le 0 ]]; then
|
|
echo "[FAIL] DiD probe: build succeeded but frontend logged no new requests" >&2
|
|
echo " (extra_hosts hijack or nginx upstream rewrite is broken)" >&2
|
|
exit 1
|
|
fi
|
|
echo "[OK] DiD inner-build flowed through cache frontend (+${delta} log lines)"
|
|
}
|