diff --git a/tools/remote b/tools/remote index 954dfd3..1fd1ac0 100755 --- a/tools/remote +++ b/tools/remote @@ -45,13 +45,22 @@ # WERKATOR_INIT_CONFIG the init fragment to apply (repo-init, instance-start) # WERKATOR_REPO_URL https clone URL of the watched repository # (default: https://github.com/mhoennig/werkator.git) +# WERKATOR_REPO_DIR directory of the watched repository, absolute or relative to +# WERKATOR_PATH (default: werkator); it also names the systemd +# unit, exactly as `init --systemd` derives it +# WERKATOR_INSTALL_DIR directory holding the unpacked runtime bundle, absolute or +# relative to WERKATOR_PATH (default: .werkator) +# WERKATOR_SANDBOX build runtime of the host: bwrap (default) or docker; a docker +# host needs neither the werkdock binary nor a rootfs archive # WERKDOCK_REPO checkout of the werkdock repository, whose binary the # instance runs (default: /../werkdock) # WERKDOCK_BINARY the built werkdock binary (default: $WERKDOCK_REPO/dist/werkdock) # WERKATOR_ROOTFS rootfs archive path for repo-init # (default: /build/werkator-buildenv-trixie-java-go-node.tar.zst) # -# Install layout on the host: +# Install layout on the host — the default, which the three keys above bend to an +# installation that predates this script (e.g. the docker host vm4006: the watched +# repository is ~/hs.hsadmin.ng, the runtime lives in ~/opt, there is no werkdock): # $WERKATOR_PATH/werkator/ the watched repository (clone) # $WERKATOR_PATH/.werkator/werkator/ the unpacked runtime bundle # $WERKATOR_PATH/.werkator/bin/ the werkdock binary @@ -113,9 +122,33 @@ HOST="$WERKATOR_REMOTE" TARGET_DIR="$WERKATOR_PATH" ROOTFS="${WERKATOR_ROOTFS:-$REPO_ROOT/build/werkator-buildenv-trixie-java-go-node.tar.zst}" REPO_URL="${WERKATOR_REPO_URL:-https://github.com/mhoennig/werkator.git}" -MACHINE_CONFIG="$TARGET_DIR/werkator/.git/werkator/.werkator.yml" -WERKATOR_BIN="$TARGET_DIR/.werkator/werkator/bin/werkator" -UNIT="werkator-werkator.service" + +# The host layout is three values, not one convention: an installation that grew +# before this script existed puts them elsewhere, and the defaults are exactly what +# `instance-install` creates, so an env file that names none of them behaves as before. +# Both directories may be absolute; a bare name is taken relative to WERKATOR_PATH. +resolve_dir() { + case "$1" in + /*) echo "$1" ;; + *) echo "$TARGET_DIR/$1" ;; + esac +} +REPO_DIR="$(resolve_dir "${WERKATOR_REPO_DIR:-werkator}")" +INSTALL_DIR="$(resolve_dir "${WERKATOR_INSTALL_DIR:-.werkator}")" +# where `repo-add` puts a further repository of the registry: beside the watched one +SIBLING_DIR="$(dirname "$REPO_DIR")" +SANDBOX="${WERKATOR_SANDBOX:-bwrap}" +case "$SANDBOX" in + bwrap|docker) ;; + *) die "WERKATOR_SANDBOX is 'bwrap' or 'docker', not '$SANDBOX'" ;; +esac + +MACHINE_CONFIG="$REPO_DIR/.git/werkator/.werkator.yml" +WERKATOR_BIN="$INSTALL_DIR/werkator/bin/werkator" +# mirrors SystemdServiceFiles.unitName: the repository's directory name, every +# character outside [A-Za-z0-9_.-] replaced by a dash — the unit `init --systemd` +# writes, which is the one this script may stop and start. +UNIT="werkator-$(basename "$REPO_DIR" | sed 's/[^A-Za-z0-9_.-]/-/g').service" ssh_present() { ssh -o BatchMode=yes -o ConnectTimeout=10 "$HOST" true 2>/dev/null @@ -133,13 +166,21 @@ ensure_ssh() { # Werkdock owns the host checks (`werkdock doctor` ports the old prerequisites # script); the binary is uploaded first, so the check works pre-install. +# On a docker host there is no werkdock and no sandbox to check: the build runtime +# is the docker daemon, so the check is that the daemon answers this user. check_prerequisites() { + if [ "$SANDBOX" = "docker" ]; then + echo "==> Checking the docker build runtime on $HOST (WERKATOR_SANDBOX=docker)" + ssh "$HOST" "docker info >/dev/null" || die "docker is not usable by this user on $HOST" + ssh "$HOST" "docker --version" + return 0 + fi ensure_werkdock_binary echo "==> Uploading werkdock and running its doctor on $HOST (target dir: $TARGET_DIR)" - ssh "$HOST" "mkdir -p '$TARGET_DIR/.werkator/bin'" - scp -q "$WERKDOCK_BINARY" "$HOST:$TARGET_DIR/.werkator/bin/werkdock.new" - ssh "$HOST" "mv '$TARGET_DIR/.werkator/bin/werkdock.new' '$TARGET_DIR/.werkator/bin/werkdock' && chmod 755 '$TARGET_DIR/.werkator/bin/werkdock'" - if ! ssh "$HOST" "'$TARGET_DIR/.werkator/bin/werkdock' doctor '$TARGET_DIR'"; then + ssh "$HOST" "mkdir -p '$INSTALL_DIR/bin'" + scp -q "$WERKDOCK_BINARY" "$HOST:$INSTALL_DIR/bin/werkdock.new" + ssh "$HOST" "mv '$INSTALL_DIR/bin/werkdock.new' '$INSTALL_DIR/bin/werkdock' && chmod 755 '$INSTALL_DIR/bin/werkdock'" + if ! ssh "$HOST" "'$INSTALL_DIR/bin/werkdock' doctor '$TARGET_DIR'"; then die "werkdock doctor failed on $HOST — install aborted" fi } @@ -160,7 +201,7 @@ next to this repository, or point WERKDOCK_REPO/WERKDOCK_BINARY at your checkout upload_fragment() { [ -n "${WERKATOR_INIT_CONFIG:-}" ] || { echo ""; return 0; } [ -f "$WERKATOR_INIT_CONFIG" ] || die "init fragment missing: $WERKATOR_INIT_CONFIG" - local remote="$TARGET_DIR/.werkator/$(basename "$WERKATOR_INIT_CONFIG")" + local remote="$INSTALL_DIR/$(basename "$WERKATOR_INIT_CONFIG")" scp -q "$WERKATOR_INIT_CONFIG" "$HOST:$remote" echo "$remote" } @@ -174,25 +215,31 @@ ensure_instance_artifacts() { (cd "$REPO_ROOT" && ./gradlew runtimeBundle --console=plain -q) fi [ -f "$RUNTIME_BUNDLE" ] || die "runtime bundle missing: $RUNTIME_BUNDLE" - ensure_werkdock_binary + [ "$SANDBOX" = "docker" ] || ensure_werkdock_binary } # Uploads and unpacks the instance artifacts. The previous runtime stays as # werkator.prev for one deployment as the rollback asset. deploy_instance() { - echo "==> Uploading runtime bundle and werkdock binary" - ssh "$HOST" "mkdir -p '$TARGET_DIR/.werkator/bin'" - scp -q "$RUNTIME_BUNDLE" "$HOST:$TARGET_DIR/.werkator/" - scp -q "$WERKDOCK_BINARY" "$HOST:$TARGET_DIR/.werkator/bin/werkdock.new" + if [ "$SANDBOX" = "docker" ]; then + echo "==> Uploading runtime bundle" + else + echo "==> Uploading runtime bundle and werkdock binary" + fi + ssh "$HOST" "mkdir -p '$INSTALL_DIR/bin'" + scp -q "$RUNTIME_BUNDLE" "$HOST:$INSTALL_DIR/" + if [ "$SANDBOX" != "docker" ]; then + scp -q "$WERKDOCK_BINARY" "$HOST:$INSTALL_DIR/bin/werkdock.new" + fi echo "==> Unpacking" ssh "$HOST" "set -e - cd '$TARGET_DIR/.werkator' - mv bin/werkdock.new bin/werkdock && chmod 755 bin/werkdock + cd '$INSTALL_DIR' + [ ! -f bin/werkdock.new ] || { mv bin/werkdock.new bin/werkdock && chmod 755 bin/werkdock; } rm -rf werkator.prev [ ! -d werkator ] || mv werkator werkator.prev tar xzf '$(basename "$RUNTIME_BUNDLE")' './werkator/bin/werkator' --version - './bin/werkdock' version" + [ ! -x bin/werkdock ] || './bin/werkdock' version" } instance_install() { @@ -203,14 +250,14 @@ instance_install() { echo echo "==> Instance installed." echo " Runtime: $WERKATOR_BIN" - echo " werkdock: $TARGET_DIR/.werkator/bin/werkdock" + [ "$SANDBOX" = "docker" ] || echo " werkdock: $INSTALL_DIR/bin/werkdock" echo " Next: tools/remote werkator repo-init, then instance-start" } # Refuse to swap the runtime under a running build; FORCE=1 overrides. require_idle() { local port - port="$(ssh "$HOST" "cd '$TARGET_DIR/werkator' 2>/dev/null && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"' || true)" + port="$(ssh "$HOST" "cd '$REPO_DIR' 2>/dev/null && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"' || true)" [ -n "$port" ] || return 0 local current current="$(ssh "$HOST" "curl -s --max-time 5 http://127.0.0.1:$port/api/builds/current" || true)" @@ -249,40 +296,45 @@ instance_update() { # writing is init's — this script transports and invokes (step 23). repo_init() { ensure_ssh - [ -f "$ROOTFS" ] || die "rootfs archive missing: $ROOTFS — build it with tools/build-bwrap-rootfs.sh or set WERKATOR_ROOTFS" + [ "$SANDBOX" = "docker" ] || [ -f "$ROOTFS" ] || + die "rootfs archive missing: $ROOTFS — build it with tools/build-bwrap-rootfs.sh or set WERKATOR_ROOTFS" ssh "$HOST" "test -x '$WERKATOR_BIN'" || die "no instance on $HOST — run instance-install first" echo "==> Cloning the watched repository" - if ssh "$HOST" "test -d '$TARGET_DIR/werkator/.git'"; then + if ssh "$HOST" "test -d '$REPO_DIR/.git'"; then echo " (already cloned, skipping)" else - ssh "$HOST" "git clone '$REPO_URL' '$TARGET_DIR/werkator'" + ssh "$HOST" "git clone '$REPO_URL' '$REPO_DIR'" fi - echo "==> Uploading the rootfs archive (skipped when unchanged)" - local rootfs_remote="$TARGET_DIR/.werkator/$(basename "$ROOTFS")" - local local_sha remote_sha - local_sha="$(sha256sum "$ROOTFS" | cut -d' ' -f1)" - remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' 2>/dev/null | cut -d' ' -f1" || true)" - if [ "$local_sha" = "$remote_sha" ]; then - echo " (already on the host, skipping)" + if [ "$SANDBOX" = "docker" ]; then + echo "==> No rootfs needed (WERKATOR_SANDBOX=docker) — the build image is the repository's own Dockerfile" else - scp -q "$ROOTFS" "$HOST:$rootfs_remote" - remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' | cut -d' ' -f1")" - [ "$local_sha" = "$remote_sha" ] || die "rootfs upload checksum mismatch" + echo "==> Uploading the rootfs archive (skipped when unchanged)" + local rootfs_remote="$INSTALL_DIR/$(basename "$ROOTFS")" + local local_sha remote_sha + local_sha="$(sha256sum "$ROOTFS" | cut -d' ' -f1)" + remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' 2>/dev/null | cut -d' ' -f1" || true)" + if [ "$local_sha" = "$remote_sha" ]; then + echo " (already on the host, skipping)" + else + scp -q "$ROOTFS" "$HOST:$rootfs_remote" + remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' | cut -d' ' -f1")" + [ "$local_sha" = "$remote_sha" ] || die "rootfs upload checksum mismatch" + fi fi echo "==> Running werkator init${WERKATOR_INIT_CONFIG:+ --apply $(basename "${WERKATOR_INIT_CONFIG}")}" local fragment_remote fragment_remote="$(upload_fragment)" - ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}" + ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}" echo "==> Verifying the effective configuration" - ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' config:print 2>/dev/null | grep -A4 'bwrap:' | head -5" + ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' config:print 2>/dev/null | grep -A4 '$SANDBOX:' | head -5" echo echo "==> Repository ready." - echo " Repo: $TARGET_DIR/werkator" + echo " Repo: $REPO_DIR" echo " Next: fill git.account/git.token in $MACHINE_CONFIG if the origin is private," echo " then tools/remote werkator instance-start" } @@ -306,10 +358,10 @@ repo_add() { ssh "$HOST" "test -x '$WERKATOR_BIN'" || die "no instance on $HOST — run instance-install first" echo "==> Cloning $url as '$name'" - if ssh "$HOST" "test -d '$TARGET_DIR/$name/.git'"; then + if ssh "$HOST" "test -d '$SIBLING_DIR/$name/.git'"; then echo " (already cloned, skipping)" else - ssh "$HOST" "git clone '$url' '$TARGET_DIR/$name'" + ssh "$HOST" "git clone '$url' '$SIBLING_DIR/$name'" fi # The instance fragment carries the sandbox policy (bwrap rootfs and werkdock @@ -318,26 +370,26 @@ repo_add() { echo "==> Running werkator init in $name${WERKATOR_INIT_CONFIG:+ --apply $(basename "${WERKATOR_INIT_CONFIG}")}" local fragment_remote fragment_remote="$(upload_fragment)" - ssh "$HOST" "cd '$TARGET_DIR/$name' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}" + ssh "$HOST" "cd '$SIBLING_DIR/$name' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}" echo "==> Checking the registry" # Grepped locally: the entry may name the path absolute or as ~/, and # matching both is easier without a second layer of remote shell quoting. if ssh "$HOST" "cat ~/.werkator.yml 2>/dev/null" | - grep -qE "path: *(~|$TARGET_DIR)/$name[[:space:]]*$"; then + grep -qE "path: *(~|$SIBLING_DIR)/$name[[:space:]]*$"; then echo " (~/.werkator.yml already names this path)" else echo " not registered yet — add this entry to ~/.werkator.yml on $HOST:" echo echo " repositories:" - echo " - path: $TARGET_DIR/$name" + echo " - path: $SIBLING_DIR/$name" echo " name: $name" echo fi echo "==> Repository prepared." - echo " Repo: $TARGET_DIR/$name" - echo " Next: fill git.account/git.token in $TARGET_DIR/$name/.git/werkator/.werkator.yml if the origin is private" + echo " Repo: $SIBLING_DIR/$name" + echo " Next: fill git.account/git.token in $SIBLING_DIR/$name/.git/werkator/.werkator.yml if the origin is private" echo " (or once for all repositories in the 'defaults' block of ~/.werkator.yml)," echo " then restart the service — the registry is read at start." } @@ -353,11 +405,11 @@ instance_start() { echo "==> Applying the instance fragment and generating the host integration (init --systemd)" local fragment_remote fragment_remote="$(upload_fragment)" - ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'} --systemd" + ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'} --systemd" - local htaccess_src="$TARGET_DIR/werkator/.git/werkator/werkator.htaccess" + local htaccess_src="$REPO_DIR/.git/werkator/werkator.htaccess" local htaccess="$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www/.htaccess" - local maintenance_src="$TARGET_DIR/werkator/.git/werkator/werkator-maintenance.html" + local maintenance_src="$REPO_DIR/.git/werkator/werkator-maintenance.html" local maintenance="$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www/werkator-maintenance.html" if ssh "$HOST" "test -f '$htaccess_src'"; then echo "==> Placing the generated Apache reverse proxy at $htaccess" @@ -368,9 +420,9 @@ instance_start() { echo "==> Linking the units into ~/.config/systemd/user and enabling the service" ssh "$HOST" "mkdir -p ~/.config/systemd/user && \ - ln -sf '$TARGET_DIR/werkator/.git/werkator/$UNIT' ~/.config/systemd/user/ && \ - ln -sf '$TARGET_DIR/werkator/.git/werkator/werkator-docker-prune.service' ~/.config/systemd/user/ && \ - ln -sf '$TARGET_DIR/werkator/.git/werkator/werkator-docker-prune.timer' ~/.config/systemd/user/ && \ + ln -sf '$REPO_DIR/.git/werkator/$UNIT' ~/.config/systemd/user/ && \ + ln -sf '$REPO_DIR/.git/werkator/werkator-docker-prune.service' ~/.config/systemd/user/ && \ + ln -sf '$REPO_DIR/.git/werkator/werkator-docker-prune.timer' ~/.config/systemd/user/ && \ XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user daemon-reload && \ XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user restart '$UNIT' && \ XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user status '$UNIT' --no-pager -l | head -12" @@ -388,7 +440,7 @@ port_forward() { # the effective port, wherever it is configured (machine config or applied # fragment) — config:print is the single answer, not this script's parser local remote_port - remote_port="$(ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"')" + remote_port="$(ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"')" [ -n "$remote_port" ] || die "no server.port configured — run 'tools/remote werkator instance-start' first" case "$COMMAND" in @@ -430,7 +482,7 @@ port_forward() { # CLI owns creation and format (step 23), this script only invokes it. control_token() { ensure_ssh - ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' control-token" + ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' control-token" } case "$REPO" in