instance-update stopped the unit and only then started the upload, so a transfer that dies mid-way leaves the host with no running Werkator and nothing to start again. That is not theoretical: deploying to vm4006 on 2026-09-03 failed with "scp: Connection closed" with the service already stopped. The upload now happens before the stop, and each artifact is transferred to a .part file whose sha256 is compared with the local one before it is moved into place, retrying twice. A truncated archive would otherwise unpack into a broken runtime, which is worse than the failed transfer it came from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
575 lines
26 KiB
Bash
Executable File
575 lines
26 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Central control script for remote Werkator operations (same pattern as the
|
|
# `remote` scripts in the other repos): the first argument is the repo selector,
|
|
# the second the command. All connection and deployment values come from the
|
|
# `.env` file in the repository root — never as command line parameters.
|
|
#
|
|
# Commands name their role (step 21 session D): `instance-*` manages the
|
|
# BUILDER — the installed Werkator instance and its werkdock sandbox tool —
|
|
# while `repo-*` acts on the BUILT, the repository the instance watches.
|
|
# Werkator is never built on the target: the instance is installed from the
|
|
# locally built runtime bundle (ADR 0006), and builds of the watched
|
|
# repository are the running instance's job (or `bin/werkator build` on the
|
|
# host — the werkator CLI, not this script).
|
|
#
|
|
# Usage (step 23: `--env-file` selects the target instance, default `.env`):
|
|
# tools/remote [--env-file FILE] werkator check-prerequisites werkdock doctor on the host
|
|
# tools/remote [--env-file FILE] werkator instance-install first-time: upload + unpack bundle and werkdock
|
|
# tools/remote [--env-file FILE] werkator instance-update redeploy bundle + werkdock, restart the service
|
|
# tools/remote [--env-file FILE] werkator instance-start apply fragment, Apache proxy, systemd unit
|
|
# tools/remote [--env-file FILE] werkator repo-init clone the watched repo, init --apply, rootfs
|
|
# tools/remote [--env-file FILE] werkator repo-add URL [NAME] clone and init ANOTHER repository for the
|
|
# registry, then print the entry to add to ~/.werkator.yml
|
|
# tools/remote [--env-file FILE] werkator control-token
|
|
# tools/remote [--env-file FILE] port-forward start background tunnel to the Werkator UI
|
|
# tools/remote [--env-file FILE] port-forward stop
|
|
#
|
|
# The env file carries TRANSPORT values only; everything that is Werkator
|
|
# configuration travels as a YAML fragment in the config schema, named by
|
|
# WERKATOR_INIT_CONFIG and installed remotely via `werkator init --apply`
|
|
# (docs/plan/23-init-owns-the-files.md). Pair the files per instance, e.g.
|
|
# `.env.mih34` + `.env.mih34.yml` (both gitignored).
|
|
#
|
|
# Required in the env file:
|
|
# WERKATOR_REMOTE user@host to operate on, e.g. mih34-werkator@mih34.hostsharing.net
|
|
# WERKATOR_PATH target directory on that host, e.g. /home/storage/mih34/users/werkator
|
|
#
|
|
# Required for `instance-start`:
|
|
# WERKATOR_DOMAIN the domain served by the managed Apache (docroot location for the
|
|
# generated .htaccess); the port lives in the fragment (server.port)
|
|
#
|
|
# Required for `port-forward`:
|
|
# WERKATOR_LOCAL_PORT the local port the browser uses
|
|
# Optional in the env file:
|
|
# 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: <repo>/../werkdock)
|
|
# WERKDOCK_BINARY the built werkdock binary (default: $WERKDOCK_REPO/dist/werkdock)
|
|
# WERKATOR_ROOTFS rootfs archive path for repo-init
|
|
# (default: <repo>/build/werkator-buildenv-trixie-java-go-node.tar.zst)
|
|
#
|
|
# 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
|
|
# $WERKATOR_PATH/.werkator/*.tar.* uploaded bundle and rootfs archives
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
|
|
ENV_FILE=""
|
|
if [ "${1:-}" = "--env-file" ]; then
|
|
ENV_FILE="${2:?missing value for --env-file}"
|
|
shift 2
|
|
fi
|
|
|
|
REPO="${1:-}"
|
|
COMMAND="${2:-}"
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
RUNTIME_BUNDLE="$REPO_ROOT/build/distributions/werkator-runtime-linux-x64.tar.gz"
|
|
# Werkdock lives in its own repository since step 21 session E. Its binary is
|
|
# built there, not here: WERKDOCK_REPO names the checkout (default: a sibling
|
|
# of this repository), WERKDOCK_BINARY the built binary within it.
|
|
WERKDOCK_REPO="${WERKDOCK_REPO:-$REPO_ROOT/../werkdock}"
|
|
WERKDOCK_BINARY="${WERKDOCK_BINARY:-$WERKDOCK_REPO/dist/werkdock}"
|
|
PID_FILE="/tmp/werkator-port-forward-$(id -u).pid"
|
|
LOG_FILE="/tmp/werkator-port-forward-$(id -u).log"
|
|
|
|
usage() {
|
|
awk 'NR > 2 && !/^#/ { exit } NR > 2 { sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"
|
|
exit 2
|
|
}
|
|
|
|
require_env() {
|
|
local missing=0
|
|
for name in "$@"; do
|
|
if [ -z "${!name:-}" ]; then
|
|
echo "ERROR: $name is not set — define it in $ENV_FILE" >&2
|
|
missing=1
|
|
fi
|
|
done
|
|
[ "$missing" -eq 0 ] || exit 1
|
|
}
|
|
|
|
[ -n "$REPO" ] && [ -n "$COMMAND" ] || usage
|
|
|
|
# Load the transport values. The selected env file (default: the .env in the
|
|
# repository root) wins over the environment — `set -a; source` assigns
|
|
# unconditionally; pick the target with --env-file rather than by exporting
|
|
# single values.
|
|
ENV_FILE="${ENV_FILE:-$REPO_ROOT/.env}"
|
|
set -a
|
|
[ -f "$ENV_FILE" ] && source "$ENV_FILE"
|
|
set +a
|
|
|
|
require_env WERKATOR_REMOTE WERKATOR_PATH
|
|
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}"
|
|
|
|
# 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
|
|
}
|
|
|
|
ensure_ssh() {
|
|
if ssh_present; then
|
|
echo "==> SSH access to $HOST: ok"
|
|
else
|
|
echo "==> No key-based SSH access yet; running ssh-copy-id (password prompt expected)"
|
|
ssh-copy-id "$HOST"
|
|
ssh_present || die "SSH access still not working after ssh-copy-id"
|
|
fi
|
|
}
|
|
|
|
# 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 '$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
|
|
}
|
|
|
|
ensure_werkdock_binary() {
|
|
if [ ! -f "$WERKDOCK_BINARY" ]; then
|
|
[ -f "$WERKDOCK_REPO/go.mod" ] || die \
|
|
"werkdock binary missing: $WERKDOCK_BINARY — clone https://git.javagil.de/mi/werkdock.git \
|
|
next to this repository, or point WERKDOCK_REPO/WERKDOCK_BINARY at your checkout"
|
|
echo "==> werkdock binary not found; building it in $WERKDOCK_REPO (go build)"
|
|
(cd "$WERKDOCK_REPO" && CGO_ENABLED=0 go build -o dist/werkdock .)
|
|
fi
|
|
[ -f "$WERKDOCK_BINARY" ] || die "werkdock binary missing: $WERKDOCK_BINARY"
|
|
}
|
|
|
|
# Uploads the init fragment named by WERKATOR_INIT_CONFIG and echoes its remote
|
|
# path; empty when no fragment is configured.
|
|
upload_fragment() {
|
|
[ -n "${WERKATOR_INIT_CONFIG:-}" ] || { echo ""; return 0; }
|
|
[ -f "$WERKATOR_INIT_CONFIG" ] || die "init fragment missing: $WERKATOR_INIT_CONFIG"
|
|
local remote="$INSTALL_DIR/$(basename "$WERKATOR_INIT_CONFIG")"
|
|
scp -q "$WERKATOR_INIT_CONFIG" "$HOST:$remote"
|
|
echo "$remote"
|
|
}
|
|
|
|
# The instance artifacts are built locally (ADR 0006): the runtime bundle via
|
|
# Gradle, the werkdock binary via the Go toolchain. Both are rebuilt when
|
|
# missing, never on the target.
|
|
ensure_instance_artifacts() {
|
|
if [ ! -f "$RUNTIME_BUNDLE" ]; then
|
|
echo "==> Runtime bundle not found; building it locally (./gradlew runtimeBundle)"
|
|
(cd "$REPO_ROOT" && ./gradlew runtimeBundle --console=plain -q)
|
|
fi
|
|
[ -f "$RUNTIME_BUNDLE" ] || die "runtime bundle missing: $RUNTIME_BUNDLE"
|
|
[ "$SANDBOX" = "docker" ] || ensure_werkdock_binary
|
|
}
|
|
|
|
# Uploads one file and verifies it arrived whole: a transfer that dies mid-way
|
|
# (scp: Connection closed) otherwise leaves a truncated archive that unpacks into
|
|
# a broken runtime. Retries twice, because a dropped WAN connection is not a reason
|
|
# to abort a deployment.
|
|
upload_verified() {
|
|
local src="$1" dest="$2"
|
|
local local_sha remote_sha attempt
|
|
local_sha="$(sha256sum "$src" | cut -d' ' -f1)"
|
|
remote_sha="$(ssh "$HOST" "sha256sum '$dest' 2>/dev/null | cut -d' ' -f1" || true)"
|
|
if [ "$local_sha" = "$remote_sha" ]; then
|
|
echo " $(basename "$src"): already on the host, skipping"
|
|
return 0
|
|
fi
|
|
for attempt in 1 2 3; do
|
|
if scp -q "$src" "$HOST:$dest.part"; then
|
|
remote_sha="$(ssh "$HOST" "sha256sum '$dest.part' 2>/dev/null | cut -d' ' -f1" || true)"
|
|
if [ "$local_sha" = "$remote_sha" ]; then
|
|
ssh "$HOST" "mv '$dest.part' '$dest'"
|
|
return 0
|
|
fi
|
|
echo " checksum mismatch after transfer $attempt of $(basename "$src")" >&2
|
|
else
|
|
echo " transfer $attempt of $(basename "$src") failed" >&2
|
|
fi
|
|
done
|
|
ssh "$HOST" "rm -f '$dest.part'" || true
|
|
die "cannot upload $src to $HOST:$dest — three attempts failed"
|
|
}
|
|
|
|
# Uploads the instance artifacts, without touching the installed runtime: the
|
|
# service keeps running until swap_instance_runtime replaces it, so a failed
|
|
# transfer costs nothing but the transfer.
|
|
upload_instance_artifacts() {
|
|
if [ "$SANDBOX" = "docker" ]; then
|
|
echo "==> Uploading runtime bundle"
|
|
else
|
|
echo "==> Uploading runtime bundle and werkdock binary"
|
|
fi
|
|
ssh "$HOST" "mkdir -p '$INSTALL_DIR/bin'"
|
|
upload_verified "$RUNTIME_BUNDLE" "$INSTALL_DIR/$(basename "$RUNTIME_BUNDLE")"
|
|
if [ "$SANDBOX" != "docker" ]; then
|
|
upload_verified "$WERKDOCK_BINARY" "$INSTALL_DIR/bin/werkdock.new"
|
|
fi
|
|
}
|
|
|
|
# Swaps in the uploaded artifacts. The previous runtime stays as werkator.prev
|
|
# for one deployment as the rollback asset.
|
|
swap_instance_runtime() {
|
|
echo "==> Unpacking"
|
|
ssh "$HOST" "set -e
|
|
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
|
|
[ ! -x bin/werkdock ] || './bin/werkdock' version"
|
|
}
|
|
|
|
instance_install() {
|
|
ensure_ssh
|
|
check_prerequisites
|
|
ensure_instance_artifacts
|
|
upload_instance_artifacts
|
|
swap_instance_runtime
|
|
echo
|
|
echo "==> Instance installed."
|
|
echo " Runtime: $WERKATOR_BIN"
|
|
[ "$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 '$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)"
|
|
if [ -n "$current" ] && [ "$current" != "[]" ]; then
|
|
[ "${FORCE:-}" = "1" ] || die "a build is running on $HOST — retry when idle, or FORCE=1 to override"
|
|
echo "==> WARNING: deploying although a build is running (FORCE=1)"
|
|
fi
|
|
}
|
|
|
|
instance_update() {
|
|
ensure_ssh
|
|
ensure_instance_artifacts
|
|
require_idle
|
|
# upload first, stop second: a transfer that fails must not leave the host
|
|
# without a running service (measured on vm4006, 2026-09-03)
|
|
upload_instance_artifacts
|
|
local was_active=0
|
|
if ssh "$HOST" "XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user is-active --quiet '$UNIT'"; then
|
|
was_active=1
|
|
fi
|
|
if [ "$was_active" = "1" ]; then
|
|
echo "==> Stopping $UNIT"
|
|
ssh "$HOST" "XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user stop '$UNIT'"
|
|
fi
|
|
swap_instance_runtime
|
|
if [ "$was_active" = "1" ]; then
|
|
echo "==> Starting $UNIT"
|
|
ssh "$HOST" "XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user start '$UNIT' && sleep 3 && systemctl --user is-active '$UNIT'"
|
|
else
|
|
echo "==> Service was not running; not started (use instance-start for the first start)"
|
|
fi
|
|
echo "==> Instance updated."
|
|
}
|
|
|
|
# Sets up the WATCHED repository: an anonymous https clone (a private origin
|
|
# gets its credentials via git.account/git.token in the machine config that
|
|
# `werkator init` creates), the werkator init with the instance fragment
|
|
# applied, and the rootfs archive for the sandbox builds. All configuration
|
|
# writing is init's — this script transports and invokes (step 23).
|
|
repo_init() {
|
|
ensure_ssh
|
|
[ "$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 '$REPO_DIR/.git'"; then
|
|
echo " (already cloned, skipping)"
|
|
else
|
|
ssh "$HOST" "git clone '$REPO_URL' '$REPO_DIR'"
|
|
fi
|
|
|
|
if [ "$SANDBOX" = "docker" ]; then
|
|
echo "==> No rootfs needed (WERKATOR_SANDBOX=docker) — the build image is the repository's own Dockerfile"
|
|
else
|
|
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 '$REPO_DIR' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}"
|
|
|
|
echo "==> Verifying the effective configuration"
|
|
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' config:print 2>/dev/null | grep -A4 '$SANDBOX:' | head -5"
|
|
|
|
echo
|
|
echo "==> Repository ready."
|
|
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"
|
|
}
|
|
|
|
# Prepare a SECOND (third, …) repository for the registry of an installed
|
|
# instance (ADR 0009): clone it next to the others and run `init` in it, so it
|
|
# has its own machine config. The registry entry itself is only PRINTED, never
|
|
# written: `~/.werkator.yml` is the instance's own file — it carries the port,
|
|
# the global concurrency and possibly shared credentials, and a script that
|
|
# edits it in place would rewrite the operator's own configuration behind their
|
|
# back. Cloning and initialising is mechanical, registering is a decision.
|
|
repo_add() {
|
|
local url="${1:-}"
|
|
[ -n "$url" ] || die "usage: tools/remote [--env-file FILE] werkator repo-add <clone-url> [name]"
|
|
local name="${2:-$(basename "$url" .git)}"
|
|
case "$name" in
|
|
*/*|"") die "the repository name is one path segment (it becomes the route segment /repos/<name>)" ;;
|
|
esac
|
|
|
|
ensure_ssh
|
|
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 '$SIBLING_DIR/$name/.git'"; then
|
|
echo " (already cloned, skipping)"
|
|
else
|
|
ssh "$HOST" "git clone '$url' '$SIBLING_DIR/$name'"
|
|
fi
|
|
|
|
# The instance fragment carries the sandbox policy (bwrap rootfs and werkdock
|
|
# binary). Without it a watched repository builds on the bare host, where the
|
|
# toolchains are not installed — the same --apply repo-init does.
|
|
echo "==> Running werkator init in $name${WERKATOR_INIT_CONFIG:+ --apply $(basename "${WERKATOR_INIT_CONFIG}")}"
|
|
local fragment_remote
|
|
fragment_remote="$(upload_fragment)"
|
|
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 ~/<name>, 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: *(~|$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: $SIBLING_DIR/$name"
|
|
echo " name: $name"
|
|
echo
|
|
fi
|
|
|
|
echo "==> Repository prepared."
|
|
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."
|
|
}
|
|
|
|
# Start the server as a systemd user unit behind the managed Apache. All
|
|
# configuration comes from the instance fragment (server.port, publicBaseUrl,
|
|
# systemd limits); init generates the units AND the .htaccess — this script
|
|
# only places and activates them (step 23).
|
|
instance_start() {
|
|
ensure_ssh
|
|
require_env WERKATOR_DOMAIN
|
|
|
|
echo "==> Applying the instance fragment and generating the host integration (init --systemd)"
|
|
local fragment_remote
|
|
fragment_remote="$(upload_fragment)"
|
|
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'} --systemd"
|
|
|
|
local htaccess_src="$REPO_DIR/.git/werkator/werkator.htaccess"
|
|
local htaccess="$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www/.htaccess"
|
|
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"
|
|
ssh "$HOST" "mkdir -p '$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www' && cp '$htaccess_src' '$htaccess' && cp '$maintenance_src' '$maintenance'"
|
|
else
|
|
echo "==> No werkator.htaccess generated (no server.publicBaseUrl configured) — skipping the Apache proxy"
|
|
fi
|
|
|
|
echo "==> Linking the units into ~/.config/systemd/user and enabling the service"
|
|
ssh "$HOST" "mkdir -p ~/.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"
|
|
|
|
echo
|
|
echo "==> Server started. Verify: https://$WERKATOR_DOMAIN/"
|
|
echo " Logs: ssh $HOST -- systemctl --user status '$UNIT'"
|
|
}
|
|
|
|
# Background SSH tunnel to the Werkator server, so the browser reaches the UI
|
|
# at http://localhost:<WERKATOR_LOCAL_PORT> without keeping a terminal busy.
|
|
# `start` runs ssh -N -L detached with a pid file; `stop` kills it.
|
|
port_forward() {
|
|
require_env WERKATOR_LOCAL_PORT
|
|
# 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 '$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
|
|
start)
|
|
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
echo "==> Port-forward already running (pid $(cat "$PID_FILE")) — http://localhost:$WERKATOR_LOCAL_PORT"
|
|
exit 0
|
|
fi
|
|
nohup ssh -N -L "$WERKATOR_LOCAL_PORT:127.0.0.1:$remote_port" "$HOST" \
|
|
>"$LOG_FILE" 2>&1 &
|
|
echo $! > "$PID_FILE"
|
|
sleep 1
|
|
if kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
echo "==> Forwarding http://localhost:$WERKATOR_LOCAL_PORT -> $HOST:127.0.0.1:$remote_port (pid $(cat "$PID_FILE"))"
|
|
else
|
|
echo "ERROR: port-forward failed to start — see $LOG_FILE" >&2
|
|
rm -f "$PID_FILE"
|
|
exit 1
|
|
fi
|
|
;;
|
|
stop)
|
|
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
kill "$(cat "$PID_FILE")"
|
|
rm -f "$PID_FILE"
|
|
echo "==> Port-forward stopped"
|
|
else
|
|
rm -f "$PID_FILE"
|
|
echo "==> Port-forward is not running"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown port-forward command: $COMMAND (use start or stop)" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Print the control token guarding the mutating build endpoints — the werkator
|
|
# CLI owns creation and format (step 23), this script only invokes it.
|
|
control_token() {
|
|
ensure_ssh
|
|
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' control-token"
|
|
}
|
|
|
|
case "$REPO" in
|
|
port-forward)
|
|
port_forward
|
|
;;
|
|
werkator)
|
|
case "$COMMAND" in
|
|
check-prerequisites)
|
|
ensure_ssh
|
|
check_prerequisites
|
|
;;
|
|
instance-install)
|
|
instance_install
|
|
;;
|
|
instance-update)
|
|
instance_update
|
|
;;
|
|
instance-start)
|
|
instance_start
|
|
;;
|
|
repo-init)
|
|
repo_init
|
|
;;
|
|
repo-add)
|
|
repo_add "${3:-}" "${4:-}"
|
|
;;
|
|
control-token)
|
|
control_token
|
|
;;
|
|
install)
|
|
die "'install' was the self-build prototype; use instance-install + repo-init (step 21 session D)"
|
|
;;
|
|
build)
|
|
die "'build' (the self-build) is retired; the instance builds pushes itself, or run '$WERKATOR_BIN build <branch>' on the host"
|
|
;;
|
|
start)
|
|
die "'start' is now 'instance-start' — commands name their role (builder vs built)"
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown command: $COMMAND" >&2
|
|
usage
|
|
;;
|
|
esac
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown repo selector: $REPO" >&2
|
|
usage
|
|
;;
|
|
esac
|