#!/bin/bash # Run one scenario against the working tree, in a container, offline. # # run.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 [--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//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||g" -e "s|$GIT_TOKEN||g" -e "s|$ROUTE||g" \ "$ROOT/serve.log" 2>/dev/null || : echo "== lab: $name exit $rc ==" exit $rc