Files
computer-playbook/scripts/tests/deploy/swarm/routine/01_bootstrap.sh
T
kevinveenbirkenbachandClaude Opus 5 863ff18085 fix(swarm-test): anchor the node trust store to the proxy that answers
Both Workspace jobs of run 33445831564 die at
roles/sys-svc-container/handlers/main.yml:9, on the first registry pull a nested
node's dockerd performs after a restart:

    docker: Error response from daemon: Get "https://registry-1.docker.io/v2/":
      tls: failed to verify certificate: x509: certificate signed by unknown authority

The image name is a red herring. rpardini/docker-registry-proxy SSL-bumps, and the
certificate it presents for registry-1.docker.io and for ghcr.io carries the same
issuer, CN=DockerMirrorBox Intermediate IA -- measured against the live proxy through
the make helper, with three controls: direct fetch on public roots returns 401, the
same fetch through the proxy on public roots only returns "self-signed certificate in
certificate chain", and --cacert /dev/null errors, proving curl honours the flag.
That last control mattered: curl consults its compiled-in CApath as well, where
update-ca-certificates has hash-linked the proxy CA, so without --capath /nonexistent
the negative control passes silently.

So a mirror-name fix would have MOVED the error from docker.io to ghcr.io rather than
removing it. What is missing is trust, and the trust anchor came from
INFINITO_CACHE_REGISTRY_CA_HOST_PATH -- a variable act does not forward, and whose
`make dotenv` rewrites the shared .env back to default.env. It can therefore name a
directory the proxy that actually answers never signed with.

The bootstrap now reads the anchor off that proxy's own /ca bind. And it stops
limping: with the frontend up and no /ca bind exposed, the run aborts naming the
reason instead of continuing untrusting and surfacing three minutes later as a
cryptic x509.

Locally the patch is a measured no-op -- the inspected value is byte-identical to the
.env value, and the render with and without it is identical -- so it changes behaviour
only where the variable is wrong, which is act.

Not proven: that /mnt/cache/registry/ca/ca.crt on the hosted runner is the root the
live proxy presents. It follows from rpardini's design and from the sibling leg that
works, but both host directories are absent from every artifact and that runner is
gone. The line "==> registry-cache CA source: /mnt/cache/registry/ca" in the next
workspace run settles it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:54:28 +02:00

125 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPOT for the cluster bring-up, one CI step: host-side compose build + up (node
# image layered on the distro's infinito image, so python + ansible + the CLI are
# baked -- no per-node .deb bootstrap), then every node concern (systemd wait,
# IPs, lab DNS, repo unpack) via compose/swarm/playbook.yml over the docker
# connection. Host-side pre-clean here is only the bind-mount dir + leftover
# containers; stale root-owned NFS writes are wiped in-node by the play (this
# often non-root act runner cannot delete them).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
set -a
# shellcheck source=scripts/tests/deploy/swarm/utils/topology/base.sh
. "${SCRIPT_DIR}/../utils/topology/base.sh"
set +a
# shellcheck source=scripts/meta/env/load.sh
source "${SCRIPT_DIR}/../../../../../scripts/meta/env/load.sh"
: "${RUNNER_TEMP:?}" "${APP_ID:?}" "${INFINITO_DOMAIN:?}" "${INFINITO_CONTAINER:?}"
if command -v apt-get >/dev/null 2>&1; then
APT_TIMEOUT=10m
APT_OPTS=(-o Acquire::Retries=5 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30)
if [ "$(id -u)" -eq 0 ]; then
timeout -k 30 "${APT_TIMEOUT}" apt-get "${APT_OPTS[@]}" update -qq || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
else
sudo -E timeout -k 30 "${APT_TIMEOUT}" apt-get "${APT_OPTS[@]}" update -qq || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
fi
fi
if docker exec "${INFINITO_CONTAINER}" docker ps --format '{{.Names}}' 2>/dev/null | grep -qx 'tor'; then
echo "==> quiescing CI compose tor (competing onion publisher)"
docker exec "${INFINITO_CONTAINER}" docker stop tor
fi
bash "${SCRIPT_DIR}/../utils/unmount/nfs_mounts.sh" "${NFS_SERVER}" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
bash "${SCRIPT_DIR}/../utils/unmount/host_state.sh" "${INFINITO_DIR_VAR_LIB:?}"
for node in "${MGR}" "${WRK1}" "${WRK2}" "${NFS_SERVER}" "${BACKUP_NODE}"; do
docker rm -f "${node}" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
done
docker volume rm "${SWARM_NAME}_nfs-export" >/dev/null 2>&1 || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../../.." && pwd)"
if [ -z "${INFINITO_IMAGE:-}" ]; then
INFINITO_IMAGE="$(bash "${REPO_ROOT}/scripts/meta/resolve/image/local.sh"):${INFINITO_IMAGE_TAG:?}"
export INFINITO_IMAGE
if ! docker image inspect "${INFINITO_IMAGE}" >/dev/null 2>&1; then
echo "==> building local infinito image ${INFINITO_IMAGE} for distro ${INFINITO_DISTRO}"
make -C "${REPO_ROOT}" build
fi
fi
COMPOSE_FILE="${SCRIPT_DIR}/../../../../../compose/swarm/compose.yml"
COMPOSE_ARGS=(-f "${COMPOSE_FILE}")
CACHE_FRONTEND="infinito-package-cache-frontend"
REGISTRY_CACHE="infinito-registry-cache"
CACHE_NET=""
if docker inspect "${CACHE_FRONTEND}" >/dev/null 2>&1; then
CACHE_NET="$(docker inspect "${CACHE_FRONTEND}" --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' | head -n1)"
echo "==> package-cache detected on ${CACHE_NET}; wiring swarm nodes to it"
registry_ca_src="$(docker inspect "${REGISTRY_CACHE}" \
--format '{{range .Mounts}}{{if eq .Destination "/ca"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || true)"
if [ -z "${registry_ca_src}" ]; then
echo "FAILURE: ${CACHE_FRONTEND} is up but ${REGISTRY_CACHE} exposes no /ca bind; the nodes cannot learn its MITM CA" >&2
exit 1
fi
export INFINITO_CACHE_REGISTRY_CA_HOST_PATH="${registry_ca_src}"
echo "==> registry-cache CA source: ${INFINITO_CACHE_REGISTRY_CA_HOST_PATH}"
COMPOSE_ARGS+=(-f "${SCRIPT_DIR}/../../../../../compose/swarm/cache.override.yml")
fi
build_attempts=3
for attempt in $(seq 1 "${build_attempts}"); do
docker compose "${COMPOSE_ARGS[@]}" -p "${SWARM_NAME}" --profile drill build && break
if [ "${attempt}" -eq "${build_attempts}" ]; then
echo "FAILURE: node image build failed after ${build_attempts} attempts" >&2
exit 1
fi
sleep $((attempt * 5))
done
docker compose "${COMPOSE_ARGS[@]}" -p "${SWARM_NAME}" --profile drill up -d
if [ -n "${CACHE_NET}" ]; then
cache_subnet="$(docker network inspect "${CACHE_NET}" \
--format '{{range .IPAM.Config}}{{println .Subnet}}{{end}}' 2>/dev/null |
grep -F . | head -n1 || true)"
case "${cache_subnet}" in
*.0/24) ;;
*)
echo "FAILURE: ${CACHE_NET} reports IPv4 subnet '${cache_subnet:-<none>}'; the node pinning below only holds for a x.y.z.0/24" >&2
exit 1
;;
esac
cache_prefix="${cache_subnet%.0/24}"
node_octet=$((200 + INFINITO_INSTANCE * 5))
if [ "$((node_octet + 4))" -gt 254 ]; then
echo "FAILURE: INFINITO_INSTANCE=${INFINITO_INSTANCE} pushes the node band past ${cache_prefix}.254" >&2
exit 1
fi
for node in "${MGR}" "${WRK1}" "${WRK2}" "${NFS_SERVER}" "${BACKUP_NODE}"; do
if ! connect_err="$(docker network connect --ip "${cache_prefix}.${node_octet}" "${CACHE_NET}" "${node}" 2>&1)"; then
case "${connect_err}" in
*"already exists in network"* | *"already attached to network"*)
echo "==> ${node} already on ${CACHE_NET}; keeping its current address" >&2
;;
*)
echo "FAILURE: cannot pin ${node} to ${cache_prefix}.${node_octet} on ${CACHE_NET}: ${connect_err}" >&2
exit 1
;;
esac
fi
node_octet=$((node_octet + 1))
done
fi
ansible-playbook \
-i "${MGR},${WRK1},${WRK2},${NFS_SERVER},${BACKUP_NODE}," \
-c docker \
"${SCRIPT_DIR}/../../../../../compose/swarm/playbook.yml"