Files
bcherb2 a001406a33 test: container lab for the two-tier apply
A disposable ubuntu container, a fake private tier and a fake bootstrap
endpoint, so the whole documented path — chezmoi init --apply, dotup pick,
dotup private, cmp apply, dotsecrets — can run end to end without touching a
real machine or a real credential. The fake tier mirrors the real one's
structure (seven secrets and one alias) because dotsecrets is copied verbatim
and the "8 exports, not 7" assertion depends on that cardinality; its ids are
sequential and obviously synthetic.

check-verbatim.sh keeps the fake tier's copies of shipped files honest, and
snapshot.sh records file modes so a 644 where a 600 belongs is a diff.
2026-08-21 22:34:53 -04:00

115 lines
4.8 KiB
Bash
Executable File

#!/bin/bash
# Run one scenario against the working tree, in a container, offline.
#
# run.sh <scenario.sh> [--image IMG] [--keep] [--priv REPO]
#
# Everything the real system talks to is stood in locally: the bootstrap
# endpoint, both git remotes, and the secrets manager. Nothing here touches the
# real endpoint, the real repos, or the network, so a scenario can be run as
# often as it takes without publishing anything or spending a password.
#
# The code under test is the CURRENT WORKING TREE, committed or not.
set -euo pipefail
cd "$(dirname "$0")"
LAB=$PWD
PUB=$(CDPATH= cd -- ../.. && pwd)
PRIV_DEFAULT=$LAB/fake-private
scenario=""; IMAGE=dotup-lab:24.04; KEEP=0; PRIV=$PRIV_DEFAULT
while [ $# -gt 0 ]; do
case $1 in
--image) IMAGE=$2; shift 2 ;;
--keep) KEEP=1; shift ;;
--priv) PRIV=$2; shift 2 ;;
-*) echo "unknown flag $1" >&2; exit 2 ;;
*) scenario=$1; shift ;;
esac
done
[ -n "$scenario" ] || { echo "usage: run.sh <scenario.sh> [--image IMG] [--keep]" >&2; exit 2; }
[ -f "$scenario" ] || { echo "no such scenario: $scenario" >&2; exit 2; }
name=$(basename "$scenario" .sh)
# Distinct per run. The route and password are secrets in production, so the
# lab never reuses a value and never hardcodes one -- a scenario that only
# passes against a fixed password is testing the fixture.
rand() { head -c 18 /dev/urandom | od -An -tx1 | tr -d ' \n'; }
ROUTE=r-$(rand); PASS=$(rand); GIT_TOKEN=$(rand); USER_=ben
GW=$(docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}')
# Left unset so the server takes any free port and reports it back; several
# scenarios run at once and a fixed port makes them collide.
PORT=${LAB_PORT:-0}
ROOT=$(mktemp -d /tmp/dotup-lab.XXXXXX)
C=dotup-lab-$name-$$
cleanup() {
rc=$?
[ -n "${SRV:-}" ] && kill "$SRV" 2>/dev/null || :
if [ "$KEEP" = 1 ]; then
echo "kept: container $C lab root $ROOT" >&2
else
docker rm -f "$C" >/dev/null 2>&1 || :
rm -rf "$ROOT"
fi
exit $rc
}
trap cleanup EXIT INT TERM
echo "== lab: snapshotting working trees =="
sh snapshot.sh "$PUB" "$ROOT/git" dotfiles-public >/dev/null
sh snapshot.sh "$PRIV" "$ROOT/git" dotfiles-private >/dev/null
echo " public: $(git -C "$ROOT/git/dotfiles-public.git" ls-tree -r --name-only HEAD | wc -l) files"
echo " private: $(git -C "$ROOT/git/dotfiles-private.git" ls-tree -r --name-only HEAD | wc -l) files ($PRIV)"
export LAB_GIT_ROOT=$ROOT/git LAB_PORT=$PORT LAB_ROUTE=$ROUTE \
LAB_USER=$USER_ LAB_PASS=$PASS LAB_GIT_TOKEN=$GIT_TOKEN LAB_BIND=$GW
# The blob is byte-for-byte the shape the real endpoint returns: two KEY=VALUE
# lines, the repo URL carrying an inline token that the installer has to split
# out into a credential file.
# {PORT} is filled in by the server once it has bound one. The blob is
# otherwise byte-for-byte the shape the real endpoint returns: two KEY=VALUE
# lines, the repo URL carrying an inline token the installer must split out.
export LAB_BLOB="PRIVATE_REPO_URL=http://git:$GIT_TOKEN@$GW:{PORT}/git/dotfiles-private.git
BWS_ACCESS_TOKEN=lab-bws-$(rand)
"
python3 serve.py >"$ROOT/serve.log" 2>&1 & SRV=$!
for _ in $(seq 40); do
PORT=$(sed -n 's/^lab: listening on [^:]*:\([0-9]*\).*/\1/p' "$ROOT/serve.log")
[ -n "$PORT" ] && [ "$PORT" != 0 ] && break
sleep 0.25
done
[ -n "$PORT" ] && [ "$PORT" != 0 ] || { echo "lab server never reported a port:"; cat "$ROOT/serve.log"; exit 1; }
for _ in $(seq 40); do
curl -sf -o /dev/null -u "$USER_:$PASS" "http://$GW:$PORT/$ROUTE/bootstrap.env" && break
sleep 0.25
done
curl -sf -o /dev/null -u "$USER_:$PASS" "http://$GW:$PORT/$ROUTE/bootstrap.env" \
|| { echo "lab server never came up:"; cat "$ROOT/serve.log"; exit 1; }
echo " endpoint up on $GW:$PORT"
docker image inspect "$IMAGE" >/dev/null 2>&1 || {
echo "== lab: building $IMAGE =="; docker build -q -t "$IMAGE" -f Dockerfile . >/dev/null; }
docker rm -f "$C" >/dev/null 2>&1 || :
docker run -d --name "$C" --add-host lab:"$GW" "$IMAGE" >/dev/null
docker cp "$scenario" "$C:/tmp/scenario.sh" >/dev/null
[ -d assets ] && docker cp assets "$C:/tmp/assets" >/dev/null
echo "== lab: $name on $IMAGE =="
# Bare -e names inherit from this shell, so the password and the git token never
# appear in docker's argv -- /proc/<pid>/cmdline is world readable, which is the
# same hole this repo was fixed to stop opening.
export BOOT_URL="http://$GW:$PORT/$ROUTE" BOOT_USER=$USER_ BOOT_PW=$PASS \
PUB_URL="http://$GW:$PORT/git/dotfiles-public.git"
set +e
docker exec -u ben \
-e BOOT_URL -e BOOT_USER -e BOOT_PW -e PUB_URL \
-e HOME=/home/ben -e LANG=en_US.UTF-8 \
"$C" bash /tmp/scenario.sh
rc=$?
set -e
# Redact before anything is printed or kept: a scenario log that quoted the
# password back would be as bad as committing it.
sed -i -e "s|$PASS|<BOOT_PW>|g" -e "s|$GIT_TOKEN|<GIT_TOKEN>|g" -e "s|$ROUTE|<ROUTE>|g" \
"$ROOT/serve.log" 2>/dev/null || :
echo "== lab: $name exit $rc =="
exit $rc