#!/usr/bin/env bash # Tries rollout.sh on this machine's docker, with requests flowing the whole # time, and counts the ones that failed: # # deploy/rollout-test.sh # # It builds the image (SKIP_BUILD=1 reuses knotless-rolltest:current), then # runs docker-compose.yml as its own project, knotless-rolltest, behind a # Caddy on its own network that proxies to knotless-serve:2590 as the # proxy in front of knotless.dev does, on 127.0.0.1:$PORT (28480). Four # clients ask for /healthz (serve alone) and / (serve, the pages, and serve # again for their data) through Caddy while it: # # 1. rolls pages and serve onto a new image, with Caddy as it is today; # 2. rolls again, with lb_try_duration on the proxy (README.md); # 3. rolls a serve whose index would be rebuilt, which stops and starts it; # 4. rolls a serve that never turns healthy, which must leave the old one; # 5. rolls a serve that cannot check the index, which must too. # # Everything it makes is removed at the end, but the image with KEEP_IMAGE=1. set -euo pipefail cd "$(dirname "$0")/.." PORT="${PORT:-28480}" PROJECT=knotless-rolltest NETWORK=knotless-rolltest-proxy IMAGE=knotless-rolltest:current WORK="$(mktemp -d)" URL="http://127.0.0.1:$PORT" cleanup() { touch "$WORK/stop" wait 2>/dev/null || true (cd "$WORK" && docker compose down -v --remove-orphans -t 5 >/dev/null 2>&1) || true docker rm -f "$PROJECT-caddy" >/dev/null 2>&1 || true docker network rm "$NETWORK" >/dev/null 2>&1 || true docker image rm "$PROJECT:broken" >/dev/null 2>&1 || true docker tag "$PROJECT:base" "$IMAGE" 2>/dev/null || true docker image rm "$PROJECT:base" >/dev/null 2>&1 || true for rev in 1 2 3; do docker image rm "$PROJECT:rev$rev" >/dev/null 2>&1 || true; done [ -n "${KEEP_IMAGE:-}" ] || docker image rm "$IMAGE" >/dev/null 2>&1 || true rm -rf "$WORK" } trap cleanup EXIT [ -n "${SKIP_BUILD:-}" ] || docker build -t "$IMAGE" . # A tag of its own: a build cannot start FROM an image ID. BASE="$PROJECT:base" docker tag "$IMAGE" "$BASE" # The deploy's compose file and rollout.sh, as deploy.sh puts them on a # host, with this project's names and paths. mkdir -p "$WORK/data" sed -e "s|^name: knotless$|name: $PROJECT|" \ -e "s|image: knotless:current|image: $IMAGE|" \ -e "s|driver: journald|driver: json-file|" \ deploy/docker-compose.yml >"$WORK/docker-compose.yml" cp deploy/rollout.sh "$WORK/" cat >"$WORK/.env" <"$WORK/Caddyfile" } docker network create "$NETWORK" >/dev/null caddyfile plain docker run -d --name "$PROJECT-caddy" --network "$NETWORK" -p "127.0.0.1:$PORT:80" \ -v "$WORK/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2 >/dev/null compose() { (cd "$WORK" && docker compose "$@"); } rollout() { (cd "$WORK" && KNOTLESS_DEPLOY_LOG="$WORK/deploy.log" sh rollout.sh "$@"); } # A new image of the same build, as a deploy that changed something makes. retag() { printf 'FROM %s\nLABEL rolltest=%s\n' "$BASE" "$1" | docker build -q -t "$PROJECT:$1" - >/dev/null docker tag "$PROJECT:$1" "$IMAGE" } load() { rm -f "$WORK/stop" "$WORK"/codes.* for w in 1 2 3 4; do path=/healthz [ $((w % 2)) -eq 0 ] && path=/ (while [ ! -f "$WORK/stop" ]; do curl -s -o /dev/null -m 30 -w '%{http_code}\n' "$URL$path" >>"$WORK/codes.$w" || true done) & done sleep 3 } report() { sleep 3 touch "$WORK/stop" wait local total failed total=$(cat "$WORK"/codes.* | wc -l | tr -d ' ') failed=$(cat "$WORK"/codes.* | grep -cvE '^[23]' || true) echo "== $1: $total requests, $failed failed ($(sort "$WORK"/codes.* | uniq -c | tr -s ' ' | tr '\n' ',' | sed 's/,$//'))" } echo "→ first start" rollout pages serve for _ in $(seq 60); do curl -sf -o /dev/null "$URL/" && break; sleep 1; done curl -sf -o /dev/null "$URL/" || { echo "the page never answered" >&2; compose logs --tail 30 >&2; exit 1; } echo "→ 1. rolling, Caddy as today" load retag rev1 rollout pages serve report "rolling, no lb_try_duration" echo "→ 2. rolling, with lb_try_duration" caddyfile retry docker exec "$PROJECT-caddy" caddy reload --config /etc/caddy/Caddyfile >/dev/null 2>&1 load retag rev2 rollout pages serve report "rolling, lb_try_duration 20s" echo "→ 3. an index the new build would rebuild: stop, then start" compose exec -T serve node -e \ "new (require('node:sqlite').DatabaseSync)('/data/index.db').exec('PRAGMA user_version = 1')" load retag rev3 rollout serve | tee "$WORK/rollout.out" report "rebuilt index, stop then start, lb_try_duration 20s" grep -q "old one stops first" "$WORK/rollout.out" || { echo "it did not stop and start" >&2; exit 1; } # A build of serve that answers the index check but never listens, and one # that cannot even check: each must leave the old serve serving. broken() { printf 'FROM %s\nENTRYPOINT %s\n' "$BASE" "$2" | docker build -q -t "$PROJECT:broken" - >/dev/null docker tag "$PROJECT:broken" "$IMAGE" before=$(compose ps -q serve) load if KNOTLESS_ROLLOUT_TIMEOUT=15 rollout serve; then echo "a broken serve was rolled to" >&2 exit 1 fi report "$1, kept the old one" [ "$(compose ps -q serve)" = "$before" ] || { echo "the old serve is not the one left" >&2; exit 1; } echo "→ kept $(echo "$before" | cut -c1-12)" } echo "→ 4. a new serve that never turns healthy" broken "never healthy" '["sh", "-c", "case \"$*\" in *--check-index*) exec \"$@\" ;; *) exec sleep 3600 ;; esac", "--"]' echo "→ 5. a new serve that cannot check the index" broken "fails the index check" '["false"]'