#!/usr/bin/env bash # Builds knotless on the runner host from this checkout and (re)starts its # knotless-run there. # # deploy/runner-host/deploy.sh # KNOTLESS_RUNNER_HOST=root@host deploy/runner-host/deploy.sh # # or with that in deploy/local.env. Run setup.sh on the host once first. # # KNOTLESS_RUNNER_ENV names a file read instead of deploy/local.env, for # a second runner on a host, as deploy/ci.env is for the trusted runner # (README.md, The trusted runner), so none of the first's settings reach # it: its KNOTLESS_RUNNER_HOST and KNOTLESS_RUNNER_* settings, and # KNOTLESS_RUNNER_NAME, _ETC and _DATA, its compose project, its directory # for run.env, the secrets and the pause list (/etc/knotless), and its # work (/var/lib/knotless-run). # # From deploy/local.env, or the environment: # # - optional: KNOTLESS_RUNNER_REPOS, repositories it watches besides # those whose records name its account, which it finds (--discover), as # `alice.example.com/tally bob.example.com/notes`; # - KNOTLESS_RUNNER_IDENTIFIER and KNOTLESS_RUNNER_PASSWORD, its account # and an app password for it, needed once: they are kept on the host in # /etc/knotless/run.env; # - optional: KNOTLESS_RUNNER_DOMAIN and KNOTLESS_RUNNER_DID, the name # Caddy answers the runner's bot login at (`runner.knotless.dev`, an # A/AAAA record to this host) and the runner's account, the bot that may # log in there: both turn on the `bots` profile, for private # repositories (README.md, Private repositories); # - optional: KNOTLESS_RUNNER_ARGS, more of its command line (`--queue`); # KNOTLESS_RUNNER_JOBS (4), _CPUS (2), _MEMORY (3, GiB), _TIMEOUT # (3600 s), _ALLOWANCE (2000) and _PRIVATE_ALLOWANCE (500), as # docker-compose.yml passes them; KNOTLESS_LOG (info); # - optional: KNOTLESS_RUNNER_IMAGE_FROM, an SSH destination whose # knotless:current to run rather than building here, copied only when # its runner's build differs: main's deploy sets it to the main host, # so both run the same knotless (.knotless/workflows/deploy.yml). # # The image is built on the host, from this checkout's HEAD as `git # archive` gives it (so commit first): a build driven over SSH by a local # docker client times out resolving base images, and Docker Desktop's # builder ignores DOCKER_HOST and builds locally. set -euo pipefail cd "$(dirname "$0")/../.." if [ -n "${KNOTLESS_RUNNER_ENV:-}" ]; then set -a; . "$KNOTLESS_RUNNER_ENV"; set +a elif [ -f deploy/local.env ]; then set -a; . deploy/local.env; set +a fi NAME="${KNOTLESS_RUNNER_NAME:-knotless-runner}" ETC="${KNOTLESS_RUNNER_ETC:-/etc/knotless}" DATA="${KNOTLESS_RUNNER_DATA:-/var/lib/knotless-run}" DEST="${1:-${KNOTLESS_RUNNER_HOST:-}}" [ -n "$DEST" ] || { sed -n '5,6p' "$0" >&2; exit 2; } REPOS="${KNOTLESS_RUNNER_REPOS:-}" SSH="ssh -o StrictHostKeyChecking=accept-new $DEST" COMMIT="$(git rev-parse HEAD 2>/dev/null || echo local)" if [ -n "${KNOTLESS_RUNNER_IMAGE_FROM:-}" ]; then # A box too small to build knotless, or a deploy of main, takes the image # another host built, unless the runner here is that build already: as # the main host's deploy does (deploy/deploy.sh), a runner whose build # is the same is left running, since a restart cancels its runs. FROM="ssh -o StrictHostKeyChecking=accept-new $KNOTLESS_RUNNER_IMAGE_FROM" PRINT='docker run --rm --entrypoint cat knotless:current /etc/knotless-run.fingerprint' NEW=$($FROM "$PRINT" 2>/dev/null || true) OLD=$($SSH "$PRINT" 2>/dev/null || true) if [[ $NEW =~ ^[0-9a-f]{64}$ ]] && [ "$NEW" = "$OLD" ]; then echo "==> its runner is ${NEW:0:12} on $KNOTLESS_RUNNER_IMAGE_FROM too: keeping its image" else echo "==> copying knotless:current (${NEW:0:12}, here ${OLD:0:12}) from $KNOTLESS_RUNNER_IMAGE_FROM to $DEST" $FROM 'docker save knotless:current | gzip -1' | $SSH 'gunzip | docker load -q' fi else echo "==> building knotless at ${COMMIT:0:12} on $DEST" git archive --format=tar HEAD | $SSH "docker build -q --build-arg KNOTLESS_COMMIT=$COMMIT -t knotless:current -" fi if [ -n "${KNOTLESS_RUNNER_IDENTIFIER:-}" ] && [ -n "${KNOTLESS_RUNNER_PASSWORD:-}" ]; then printf 'KNOTLESS_IDENTIFIER=%s\nKNOTLESS_PASSWORD=%s\n' "$KNOTLESS_RUNNER_IDENTIFIER" "$KNOTLESS_RUNNER_PASSWORD" | $SSH "umask 077; mkdir -p '$ETC' && cat > '$ETC/run.env'" fi $SSH "[ -f '$ETC/run.env' ]" || { echo "the runner needs KNOTLESS_RUNNER_IDENTIFIER and KNOTLESS_RUNNER_PASSWORD once" >&2 exit 2 } { echo "KNOTLESS_RUNNER_NAME=$NAME" echo "KNOTLESS_RUNNER_ETC=$ETC" echo "KNOTLESS_RUNNER_DATA=$DATA" echo "KNOTLESS_RUN_REPOS=$REPOS" [ -z "${KNOTLESS_RUNNER_ARGS:-}" ] || echo "KNOTLESS_RUN_ARGS=$KNOTLESS_RUNNER_ARGS" # KNOTLESS_RUNNER_DISCOVER set empty watches only KNOTLESS_RUNNER_REPOS. [ -z "${KNOTLESS_RUNNER_DISCOVER+set}" ] || echo "KNOTLESS_RUN_DISCOVER=$KNOTLESS_RUNNER_DISCOVER" for v in JOBS OWNER_JOBS CPUS MEMORY TIMEOUT ALLOWANCE PRIVATE_ALLOWANCE; do from="KNOTLESS_RUNNER_$v" [ -z "${!from:-}" ] || echo "KNOTLESS_RUN_$v=${!from}" done [ -z "${KNOTLESS_LOG:-}" ] || echo "KNOTLESS_LOG=$KNOTLESS_LOG" if [ -n "${KNOTLESS_RUNNER_DOMAIN:-}" ]; then echo "COMPOSE_PROFILES=bots" echo "KNOTLESS_RUNNER_DOMAIN=$KNOTLESS_RUNNER_DOMAIN" echo "KNOTLESS_BOT_DIDS=${KNOTLESS_RUNNER_DID:?KNOTLESS_RUNNER_DOMAIN takes KNOTLESS_RUNNER_DID, the bot that logs in there}" fi } | $SSH "umask 077; cat > '$ETC/.env'" scp -q -o StrictHostKeyChecking=accept-new deploy/runner-host/docker-compose.yml \ deploy/runner-host/Caddyfile "$DEST:$ETC/" echo "==> starting" $SSH " set -e cd '$ETC' install -d -m 0700 '$DATA' [ -f paused.json ] || echo '{}' > paused.json [ -f run-secrets.json ] || (umask 077; echo '{}' > run-secrets.json) mkdir -p '$DATA/bots/sessions' /var/lib/knotless-serve /var/lib/caddy # bots runs as uid 1000, the node user the image runs as, as the main # host's serve does; run, as root, reads and refreshes there too. chown 1000:1000 /var/lib/knotless-serve '$DATA/bots' '$DATA/bots/sessions' docker compose config --quiet docker compose up -d --remove-orphans docker compose ps "