Step 23 sessions B+C: tools/remote wraps, verified live on mih34

The wrapper's config writing is gone: --env-file FILE selects the
target instance (default .env; named like docker's flag — --env means a
single variable there), the init fragment named by WERKATOR_INIT_CONFIG
is uploaded and installed remotely via 'werkator init --apply', and
instance-start places the .htaccess that init now generates. The
heredoc/sed machine-config writing is deleted; control-token delegates
to the werkator CLI; check-prerequisites uploads werkdock and runs its
doctor — tools/werkator-build-prerequisites.sh retires. The idle-check
and port-forward port lookups read the effective config via
config:print, so a port living in the applied fragment is found too.

Verified live against mih34 with the .env.mih34 + .env.mih34.yml pair
(gitignored via the new /.env.* rule): update, doctor PASS 6/6,
repo-init applying the fragment as the new layer, instance-start
placing the generated proxy and restarting the unit, control-token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-09-01 19:00:38 +02:00
co-authored by Claude Fable 5
parent 5f1a669771
commit 1d1bd19e10
7 changed files with 114 additions and 283 deletions
+1
View File
@@ -35,4 +35,5 @@ replay_pid*
# Other
/.local/
/.env
/.env.*
@@ -42,7 +42,7 @@ Good:
Bad:
- The rootfs archive is project infrastructure that must be built and uploaded (~1.5 GB unpacked, disk/quota checked by `tools/werkator-build-prerequisites.sh`).
- The rootfs archive is project infrastructure that must be built and uploaded (~1.5 GB unpacked, disk/quota checked by `werkdock doctor`, which ported the original prerequisites script).
- Filesystem-only isolation: network and process view are the host's — acceptable here, and pinned so a branch cannot widen it, but weaker than Docker.
- Mountpoint pre-creation and mount ordering are subtle (hardened on the real webspace; see the fix messages preserved in commit `71f1fc6`).
+1 -1
View File
@@ -421,7 +421,7 @@ All Werkator containers carry `org.hoennig.werkator` labels; stale build contain
With `bwrap.enabled`, Werkator runs the build in a bubblewrap sandbox instead of native execution.
This is the third runtime, for hosts without root and without a Docker daemon (e.g. Hostsharing managed webspaces); see `docs/plan/17-bwrap-build-runtime.md` and ADR 0008.
Since step 21 session C the sandbox is executed by the `werkdock` CLI (`bwrap.werkdock`, default: resolved via `PATH`) — Werkator no longer invokes `bwrap` itself; `bwrap` must be installed for werkdock.
`werkdock doctor` checks the host's capability, superseding `tools/werkator-build-prerequisites.sh`.
`werkdock doctor` checks the host's capability (it replaced the retired `tools/werkator-build-prerequisites.sh` in step 23).
`bwrap.rootfs` names the prepared root filesystem archive — a Debian-base rootfs with the build tools (JDK, git, locales, project-specific tooling) built elsewhere, since `debootstrap` is unavailable on the target.
It is a local path or an `http(s)` URL; a URL is downloaded once into `.git/werkator/buildenv/`.
+9 -7
View File
@@ -268,14 +268,16 @@ The third deployment variant (plan step 21, verified live on a real webspace): n
Werkator runs as a systemd *user* service on the assigned localhost port ("eigener Serverdienst"), the platform's managed Apache terminates TLS and proxies via `.htaccess`, and builds run in the bubblewrap sandbox executed by the [werkdock](../werkdock/README.md) CLI (ADR 0008, step 21 session C).
Werkator is never built on the webspace: the runtime bundle and the werkdock binary are built locally and uploaded (ADR 0006).
All steps are driven by `tools/remote`, configured through the `.env` file in the repository root; commands name their role — `instance-*` manages the installed Werkator, `repo-*` the repository it watches.
All steps are driven by `tools/remote`; commands name their role — `instance-*` manages the installed Werkator, `repo-*` the repository it watches.
Each instance is a pair of files (step 23): a transport env file selected with `--env-file` (default `.env`), and a YAML fragment in the configuration schema, named by its `WERKATOR_INIT_CONFIG` key and installed remotely via `werkator init --apply` — e.g. `.env.mih34` + `.env.mih34.yml`, both gitignored.
The fragment carries the Werkator configuration (`server.port`, `publicBaseUrl`, systemd limits, `builds.default.bwrap.*`); the env file only says where and how to reach the host.
```bash
tools/remote werkator check-prerequisites # bwrap capability, disk and quota headroom
tools/remote werkator instance-install # upload + unpack the runtime bundle and werkdock
tools/remote werkator repo-init # clone the watched repo, init, rootfs archive, bwrap config
tools/remote werkator instance-start # server config, Apache proxy, systemd user unit
tools/remote port-forward start # browser tunnel while no public domain is set up
tools/remote --env-file .env.mih34 werkator check-prerequisites # uploads werkdock, runs its doctor
tools/remote --env-file .env.mih34 werkator instance-install # upload + unpack the runtime bundle and werkdock
tools/remote --env-file .env.mih34 werkator repo-init # clone the watched repo, rootfs archive, init --apply
tools/remote --env-file .env.mih34 werkator instance-start # init --apply --systemd, place .htaccess, enable the unit
tools/remote --env-file .env.mih34 port-forward start # browser tunnel while no public domain is set up
```
Layout on the host: the watched repository at `$WERKATOR_PATH/werkator/`, the unpacked runtime at `$WERKATOR_PATH/.werkator/werkator/`, the werkdock binary at `$WERKATOR_PATH/.werkator/bin/werkdock`.
@@ -285,7 +287,7 @@ Fill `git.account`/`git.token` in the machine config when the origin is private,
Updates are one command, refused while a build runs (`FORCE=1` overrides):
```bash
tools/remote werkator instance-update
tools/remote --env-file .env.mih34 werkator instance-update
```
The previous runtime stays as `.werkator/werkator.prev` for one deployment as the rollback asset.
+7 -6
View File
@@ -15,7 +15,7 @@ Werkator becomes the executing app wherever possible; `tools/remote` shrinks to
Parameters travel as **files**, not as many CLI options — and each side gets the format that is native to it (refined 2026-09-01):
- The **wrapper** keeps a small, bash-sourceable env file with the transport values only: `tools/remote --env .env.mih34 werkator repo-init` selects the target (default: `.env`), so several instances (`.env.mih34`, `.env.vm4006`, later a Werkbaum instance) are files, not edits.
- The **wrapper** keeps a small, bash-sourceable env file with the transport values only: `tools/remote --env-file .env.mih34 werkator repo-init` selects the target (default: `.env`; named `--env-file` like docker's flag for the same thing, since `--env` means a single variable there), so several instances (`.env.mih34`, `.env.vm4006`, later a Werkbaum instance) are files, not edits.
- **Werkator** takes a **YAML fragment in its own config schema**: `werkator init --apply mih34.yml` deep-merges the fragment into the machine config, idempotently — creating sections that are missing, updating the given values, never duplicating.
No mapping table exists: the fragment says `server: {port: …}` and `builds: {default: {bwrap: …}}` directly, is validated by the existing schema binding, and is documented by the existing `docs/configuration.md`.
- The wrapper uploads the fragment alongside the artifacts and calls `werkator init --apply …` remotely — the heredocs and `sed` calls in `tools/remote` disappear.
@@ -38,13 +38,13 @@ The removed legacy env-to-YAML conversion stays removed — there is no conversi
- New subcommand `werkator control-token`: print the token, creating it exactly like `ControlTokenService` does — the bash duplication in the wrapper dies.
- Tests per the writing-tests conventions; `docs/bootstrapping.md` documents `--apply` (the fragment keys need no new reference — they are ordinary `docs/configuration.md` keys).
### B — Wrapper side
### B — Wrapper side (implemented 2026-09-01 on branch `init-apply-config`)
- `tools/remote --env FILE` (default `.env`); the init fragment named by `WERKATOR_INIT_CONFIG` is uploaded, and the remote init runs with `--apply`.
- `tools/remote --env-file FILE` (default `.env`); the init fragment named by `WERKATOR_INIT_CONFIG` is uploaded, and the remote init runs with `--apply`.
- `repo-init` and `instance-start` lose their heredoc/`sed` config writing; `control-token` delegates to the new subcommand.
- `check-prerequisites` uploads the werkdock binary first and runs `werkdock doctor`; `tools/werkator-build-prerequisites.sh` retires (its werkdock port is the survivor).
### C — Live verification and docs
### C — Live verification and docs (done 2026-09-01)
- Run the full wrapper flow against mih34 (`instance-update`, `repo-init`, `instance-start` as no-op re-runs); `docs/deployment.md`'s webspace section switches to the `--env` invocations.
@@ -53,5 +53,6 @@ The removed legacy env-to-YAML conversion stays removed — there is no conversi
- Session A: done 2026-09-01 — `werkator init --apply …` installs and replaces a fragment idempotently; `werkator control-token` exists; full suite green.
Deviation from the sketch above: the fragment is NOT merged into the machine config — it is installed verbatim as its own layer (`.git/werkator/.werkator.applied.yml`, above project, below machine config), because an in-place merge would re-serialize the machine config, destroying its comments and rewriting the file that holds the secrets; a verbatim copy also makes re-apply a plain file replacement.
The `.htaccess` decision fell as proposed: generated beside the units by `init --systemd` whenever a `publicBaseUrl` is configured; the wrapper copies it into the domain docroot.
- Session B: `tools/remote` contains no YAML heredocs and no `sed` into the machine config; the prerequisites bash script is gone.
- Session C: the mih34 re-runs change nothing on a configured host and the deployment docs show only `--env`-style calls.
- Session B: done 2026-09-01 — `tools/remote` contains no YAML heredocs and no `sed` into the machine config (the port lookups for idle check and port-forward read the *effective* config via `config:print`, so a port living in the applied fragment is found too); the prerequisites bash script is gone.
- Session C: done 2026-09-01 — verified live on mih34 with the `.env.mih34` + `.env.mih34.yml` pair: instance-update, doctor-based check-prerequisites (PASS 6/6), repo-init applying the fragment, instance-start placing the generated `.htaccess` and restarting the unit, control-token via the CLI; `docs/deployment.md` shows only `--env-file`-style calls.
Known niggle: validating a fragment that carries a `builds.default` without triggers logs the loader's "no build defines onPush" warning, although a fragment is judged out of context — cosmetic, fix when it annoys.
+95 -104
View File
@@ -13,33 +13,38 @@
# repository are the running instance's job (or `bin/werkator build` on the
# host — the werkator CLI, not this script).
#
# Usage:
# tools/remote werkator check-prerequisites bwrap capability + disk/quota on the host
# tools/remote werkator instance-install first-time: upload + unpack bundle and werkdock
# tools/remote werkator instance-update redeploy bundle + werkdock, restart the service
# tools/remote werkator instance-start server config, Apache proxy, systemd unit
# tools/remote werkator repo-init clone the watched repo, init, rootfs, bwrap config
# tools/remote werkator control-token
# tools/remote port-forward start background tunnel to the Werkator UI
# tools/remote port-forward stop
# 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 control-token
# tools/remote [--env-file FILE] port-forward start background tunnel to the Werkator UI
# tools/remote [--env-file FILE] port-forward stop
#
# Required in .env:
# 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_PORT the localhost port assigned by Hostsharing (eigener Serverdienst)
# WERKATOR_DOMAIN the domain served by the managed Apache, e.g. ci.example.de
# 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 .env:
# WERKATOR_REPO_URL https clone URL of the watched repository
# (default: https://github.com/mhoennig/werkator.git)
# WERKATOR_MEMORY_MAX systemd MemoryMax for the unit, e.g. 1G (instance-start)
# WERKATOR_TASKS_MAX systemd TasksMax for the unit, e.g. 512 (instance-start)
# WERKATOR_ROOTFS rootfs archive path for repo-init
# (default: <repo>/build/werkator-buildenv-trixie-java-go-node.tar.zst)
# 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_ROOTFS rootfs archive path for repo-init
# (default: <repo>/build/werkator-buildenv-trixie-java-go-node.tar.zst)
#
# Install layout on the host:
# $WERKATOR_PATH/werkator/ the watched repository (clone)
@@ -52,11 +57,16 @@ 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)"
PREREQ_SCRIPT="$REPO_ROOT/tools/werkator-build-prerequisites.sh"
RUNTIME_BUNDLE="$REPO_ROOT/build/distributions/werkator-runtime-linux-x64.tar.gz"
WERKDOCK_BINARY="$REPO_ROOT/werkdock/dist/werkdock"
PID_FILE="/tmp/werkator-port-forward-$(id -u).pid"
@@ -71,7 +81,7 @@ require_env() {
local missing=0
for name in "$@"; do
if [ -z "${!name:-}" ]; then
echo "ERROR: $name is not set — define it in $REPO_ROOT/.env" >&2
echo "ERROR: $name is not set — define it in $ENV_FILE" >&2
missing=1
fi
done
@@ -80,10 +90,11 @@ require_env() {
[ -n "$REPO" ] && [ -n "$COMMAND" ] || usage
# Load the connection and deployment values; explicit environment wins, the
# .env in the repository root fills the rest.
# Load the transport values; explicit environment wins, the selected env file
# (default: the .env in the repository root) fills the rest.
ENV_FILE="${ENV_FILE:-$REPO_ROOT/.env}"
set -a
[ -f "$REPO_ROOT/.env" ] && source "$REPO_ROOT/.env"
[ -f "$ENV_FILE" ] && source "$ENV_FILE"
set +a
require_env WERKATOR_REMOTE WERKATOR_PATH
@@ -109,16 +120,37 @@ ensure_ssh() {
fi
}
# Run the prerequisites script remotely by piping it over stdin; TARGET_DIR and
# ROOTFS_ARCHIVE are passed as arguments to `bash -s --`.
# Werkdock owns the host checks (`werkdock doctor` ports the old prerequisites
# script); the binary is uploaded first, so the check works pre-install.
check_prerequisites() {
echo "==> Checking prerequisites on $HOST (target dir: $TARGET_DIR)"
local rootfs_remote="$TARGET_DIR/.werkator/$(basename "$ROOTFS")"
if ! ssh "$HOST" "WERKATOR_SSH_TARGET='$HOST' bash -s -- '$TARGET_DIR' '$rootfs_remote'" < "$PREREQ_SCRIPT"; then
die "prerequisites failed on $HOST — install aborted"
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
die "werkdock doctor failed on $HOST — install aborted"
fi
}
ensure_werkdock_binary() {
if [ ! -f "$WERKDOCK_BINARY" ]; then
echo "==> werkdock binary not found; building it locally (go build)"
(cd "$REPO_ROOT/werkdock" && 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="$TARGET_DIR/.werkator/$(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.
@@ -128,11 +160,7 @@ ensure_instance_artifacts() {
(cd "$REPO_ROOT" && ./gradlew runtimeBundle --console=plain -q)
fi
[ -f "$RUNTIME_BUNDLE" ] || die "runtime bundle missing: $RUNTIME_BUNDLE"
if [ ! -f "$WERKDOCK_BINARY" ]; then
echo "==> werkdock binary not found; building it locally (go build)"
(cd "$REPO_ROOT/werkdock" && CGO_ENABLED=0 go build -o dist/werkdock .)
fi
[ -f "$WERKDOCK_BINARY" ] || die "werkdock binary missing: $WERKDOCK_BINARY"
ensure_werkdock_binary
}
# Uploads and unpacks the instance artifacts. The previous runtime stays as
@@ -168,7 +196,7 @@ instance_install() {
# Refuse to swap the runtime under a running build; FORCE=1 overrides.
require_idle() {
local port
port="$(ssh "$HOST" "awk '/^server:/{f=1;next} f && /^ port:/{print \$2; exit}' '$MACHINE_CONFIG' 2>/dev/null" || true)"
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)"
[ -n "$port" ] || return 0
local current
current="$(ssh "$HOST" "curl -s --max-time 5 http://127.0.0.1:$port/api/builds/current" || true)"
@@ -202,8 +230,9 @@ instance_update() {
# 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, the rootfs archive for the
# sandbox builds, and the machine-local bwrap configuration.
# `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
[ -f "$ROOTFS" ] || die "rootfs archive missing: $ROOTFS — build it with tools/build-bwrap-rootfs.sh or set WERKATOR_ROOTFS"
@@ -216,9 +245,6 @@ repo_init() {
ssh "$HOST" "git clone '$REPO_URL' '$TARGET_DIR/werkator'"
fi
echo "==> Running werkator init"
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init"
echo "==> Uploading the rootfs archive (skipped when unchanged)"
local rootfs_remote="$TARGET_DIR/.werkator/$(basename "$ROOTFS")"
local local_sha remote_sha
@@ -232,25 +258,10 @@ repo_init() {
[ "$local_sha" = "$remote_sha" ] || die "rootfs upload checksum mismatch"
fi
echo "==> Writing the machine-local bwrap configuration"
# NOTE: the guard must match the block's real indentation — a mismatch here
# once appended the block on every run.
if ssh "$HOST" "grep -q '^ bwrap:' '$MACHINE_CONFIG' 2>/dev/null"; then
echo " (bwrap block present, skipping)"
else
ssh "$HOST" "cat >> '$MACHINE_CONFIG' <<'CFG'
# Build in the bubblewrap sandbox instead of natively (ADR 0008), executed by
# the werkdock CLI (step 21 session C). All three keys are pinned: read from
# this machine config even if a branch sets its own values.
builds:
default:
bwrap:
enabled: true
rootfs: $rootfs_remote
werkdock: $TARGET_DIR/.werkator/bin/werkdock
CFG"
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'}"
echo "==> Verifying the effective configuration"
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' config:print 2>/dev/null | grep -A4 'bwrap:' | head -5"
@@ -262,47 +273,28 @@ CFG"
echo " then tools/remote werkator instance-start"
}
# Start the server as a systemd user unit behind the managed Apache.
# WERKATOR_MEMORY_MAX / WERKATOR_TASKS_MAX (optional) are written into the
# machine config so `init --systemd` bakes them into the unit.
# 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_PORT WERKATOR_DOMAIN
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 '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'} --systemd"
local htaccess_src="$TARGET_DIR/werkator/.git/werkator/werkator.htaccess"
local htaccess="$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www/.htaccess"
echo "==> Writing server settings to the machine config"
if ssh "$HOST" "grep -q '^server:' '$MACHINE_CONFIG' 2>/dev/null"; then
# re-run: update port and publicBaseUrl in place (systemd limits stay as written)
ssh "$HOST" "sed -i 's/^ port: .*/ port: $WERKATOR_PORT/; s|^ publicBaseUrl: .*| publicBaseUrl: \"https://$WERKATOR_DOMAIN/\"|' '$MACHINE_CONFIG'"
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'"
else
ssh "$HOST" "cat >> '$MACHINE_CONFIG' <<'CFG'
# Web access: the managed Apache terminates TLS and proxies to the localhost
# port assigned by Hostsharing (eigener Serverdienst); TLS is the domain's
# Let's Encrypt certificate, so Werkator itself stays on 127.0.0.1.
server:
port: $WERKATOR_PORT
bindAddress: 127.0.0.1
publicBaseUrl: \"https://$WERKATOR_DOMAIN/\"
nginx:
enabled: false
systemd:
memoryMax: \"${WERKATOR_MEMORY_MAX:-}\"
tasksMax: \"${WERKATOR_TASKS_MAX:-}\"
CFG"
echo "==> No werkator.htaccess generated (no server.publicBaseUrl configured) — skipping the Apache proxy"
fi
echo "==> Writing the Apache reverse proxy to $htaccess"
ssh "$HOST" "mkdir -p '$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www' && cat > '$htaccess' <<'HT'
DirectoryIndex disabled
RewriteEngine On
RewriteBase /
RewriteRule .* http://127.0.0.1:$WERKATOR_PORT%{REQUEST_URI} [proxy]
HT"
echo "==> Generating the systemd user unit (init --systemd)"
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init --systemd"
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/ && \
@@ -322,9 +314,11 @@ HT"
# `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" "awk '/^server:/{f=1;next} f && /^ port:/{print \$2; exit}' '$MACHINE_CONFIG'")"
[ -n "$remote_port" ] || die "no server.port in the machine config — run 'tools/remote werkator instance-start' first"
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 '"')"
[ -n "$remote_port" ] || die "no server.port configured — run 'tools/remote werkator instance-start' first"
case "$COMMAND" in
start)
@@ -361,14 +355,11 @@ port_forward() {
esac
}
# Print the control token guarding the mutating build endpoints. If the server
# has not created it yet (it does so on first use), generate one in place — the
# server reads the file lazily, so a pre-created token is equivalent.
# 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
local token_file="$TARGET_DIR/werkator/.git/werkator/control-token"
ssh "$HOST" "if [ -f '$token_file' ]; then cat '$token_file'; else \
umask 077 && head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \\n' > '$token_file' && cat '$token_file'; fi"
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' control-token"
}
case "$REPO" in
-164
View File
@@ -1,164 +0,0 @@
#!/usr/bin/env bash
#
# Verify the bwrap (bubblewrap) build precondition on a target host before
# running Werkator's bwrap build runtime there (step 17 / ADR 0007).
#
# The whole "build Werkator inside bubblewrap on a Managed Webspace" approach
# hinges on one hard precondition: unprivileged user namespaces with a uid-0
# mapping and read-only root binds must work. This script runs the exact
# command line recorded in docs/plan/17-bwrap-build-runtime.md, checks the
# expected signals, and additionally verifies the disk/quota situation:
# a bwrap build unpacks the rootfs (a zstd archive expands to several GiB)
# plus a Gradle distribution and per-branch caches, so the host needs both
# raw free space and enough group-quota headroom.
#
# Run this ON the target host (the webspace), no root needed.
#
# Optional: the rootfs archive to size the disk/quota check against, e.g.
# werkator-build-prerequisites.sh /path/to/werkator-buildenv-trixie.tar.zst
# When omitted, the check runs against a conservative default footprint.
#
# Usage: werkator-build-prerequisites.sh [TARGET_DIR] [ROOTFS_ARCHIVE]
#
# TARGET_DIR is the directory the build workspace will live in (default: $HOME).
# The check verifies it sits on the home filesystem and has enough free space.
# ROOTFS_ARCHIVE, when given, is the rootfs archive that will be used there.
#
# Output is one PASS/FAIL line per check plus a final RESULT line, e.g.:
# PASS: bwrap version: bubblewrap 0.8.0
# PASS: build runs as root inside the namespace (uid 0)
# PASS: uid_map maps root back to the unprivileged user (uid 120957)
# PASS: read-only root bind is enforced
# PASS: at least 5 GiB free space on the build working filesystem
# FAIL: group quota headroom below the 5 GiB build footprint ...
# RESULT: FAIL (4/5) — Werkator bubblewrap builds are not usable on this host.
#
set -euo pipefail
target_dir_arg="${1:-}"
rootfs_arg="${2:-}"
die() { echo "ERROR: $*" >&2; exit 1; }
# Disk footprint a bwrap build needs headroom for, in 1K blocks: unpacked
# rootfs (zstd expands roughly 3-4x), Gradle distribution + per-branch cache,
# build output and artifacts. ~5 GiB.
MIN_FREE_BLOCKS=$((5 * 1024 * 1024))
# Reference filesystem: the one the invoking user's home directory lives on.
# Builds (repo clone, buildenv, caches) must run there — other mounts, such as
# a slow mass-storage volume, are rejected.
HOME_FS="$(df -Pk "$HOME" 2>/dev/null | awk 'NR==2 {print $1}')"
pass=0
fail=0
result() { # result PASS|FAIL "message"
echo "$1: $2"
if [ "$1" = "PASS" ]; then pass=$((pass+1)); else fail=$((fail+1)); fi
}
command -v bwrap >/dev/null 2>&1 || die "bwrap is not installed on this host"
output="$(bwrap --unshare-user --unshare-pid --die-with-parent --uid 0 --gid 0 \
--ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \
sh -c 'id -u && cat /proc/self/uid_map && (touch /usr/ro-test 2>&1 || true)' 2>&1)" ||
die "bwrap invocation failed (no user namespace support?): $output"
# Signal 0: bwrap itself is usable (version as a visible marker).
result PASS "bwrap version: $(bwrap --version 2>&1)"
# Signal 1: runs as root (uid 0) inside the namespace.
first="$(printf '%s\n' "$output" | sed -n '1p')"
if [ "$first" = "0" ]; then
result PASS "build runs as root inside the namespace (uid 0)"
else
result FAIL "expected uid 0 inside the namespace, got: $first"
fi
# Signal 2: uid_map maps root to the invoking unprivileged user.
uid_line="$(printf '%s\n' "$output" | sed -n '2p')"
self_uid="$(id -u)"
if printf '%s\n' "$uid_line" | grep -E "^[[:space:]]*0[[:space:]]+${self_uid}[[:space:]]+1" >/dev/null; then
result PASS "uid_map maps root back to the unprivileged user (uid $self_uid)"
else
result FAIL "expected uid_map '0 $self_uid 1', got: $uid_line"
fi
# Signal 3: the read-only root bind is enforced (a write to /usr fails).
if printf '%s\n' "$output" | grep -qi "read-only file system"; then
result PASS "read-only root bind is enforced"
else
result FAIL "the read-only root bind did not reject a write to /usr"
fi
# --- Disk / quota checks ------------------------------------------------
target_dir="${target_dir_arg:-$HOME}"
target_dir="$(realpath -m "$target_dir")"
min_gib=$((MIN_FREE_BLOCKS / 1024 / 1024))
if [ -n "$rootfs_arg" ] && [ ! -f "$rootfs_arg" ]; then
echo "WARNING: rootfs archive not found: $rootfs_arg (continuing without it)"
fi
target_fs="$(df -Pk "$target_dir" 2>/dev/null | awk 'NR==2 {print $1}')"
df_output="$(df -Pk "$target_dir" 2>/dev/null | awk 'NR==2 {print int($4) " " $6}')"
if [ -n "$df_output" ]; then
avail_k="${df_output%% *}"
mount="${df_output##* }"
if [ -n "$HOME_FS" ] && [ "$target_fs" != "$HOME_FS" ]; then
# An explicitly chosen foreign filesystem is allowed (e.g. for testing)
# but flagged: builds there will be slow.
echo "WARNING: target dir is on $target_fs (mounted at $mount), not the home filesystem ($HOME_FS) — builds will run on slower storage"
fi
if [ "${avail_k:-0}" -lt "$MIN_FREE_BLOCKS" ]; then
result FAIL "less than ${min_gib} GiB free space on the build working filesystem ($mount)"
else
result PASS "at least ${min_gib} GiB free space on the build working filesystem ($mount, device $target_fs)"
fi
else
echo "WARNING: could not measure free space on $target_dir — only the quota check below applies"
fi
if quota_output="$(quota -g 2>/dev/null)" && [ -n "$quota_output" ]; then
quota_ok=1
quota_seen=0
detail=""
while read -r fs blocks quota_limit; do
quota_seen=1
# Only the quota of the target filesystem counts — other volumes may
# legitimately be full or unquota'd without affecting the build.
if [ -n "$target_fs" ] && [ "$(basename "$fs")" != "$(basename "$target_fs")" ] && [ "$fs" != "$target_fs" ]; then
continue
fi
headroom=$((quota_limit - blocks))
if [ "$headroom" -lt "$MIN_FREE_BLOCKS" ]; then
quota_ok=0
detail+=" $(basename "$fs"): $(awk -v b="$headroom" 'BEGIN{printf "%.1f", b/1024/1024}') GiB free of quota;"
fi
done < <(printf '%s\n' "$quota_output" | awk '
NF==1 && $1 ~ /^\// { pending_fs=$1; next }
$1 ~ /^\// && $2 ~ /^[0-9]+$/ { print $1, $2, $4; pending_fs=""; next }
$1 ~ /^[0-9]+[*]?/ && pending_fs != "" { gsub(/\*/, "", $1); print pending_fs, $1, $3; pending_fs="" }')
if [ "$quota_seen" -eq 0 ]; then
echo "WARNING: quota tooling present but no group quota lines could be parsed — only free space was checked"
elif [ "$quota_ok" -eq 1 ]; then
result PASS "group quota headroom covers the ${min_gib} GiB build footprint"
else
result FAIL "group quota headroom below the ${min_gib} GiB build footprint (rootfs + Gradle cache); raise the quota before building.$detail"
fi
else
echo "WARNING: no readable group quota tooling on this host — only free space was checked"
fi
total=$((pass + fail))
echo
if [ "$fail" -eq 0 ]; then
echo "RESULT: PASS ($pass/$total) — Werkator bubblewrap builds are usable on this host."
echo "Next: install the Werkator instance with: tools/remote werkator install ${WERKATOR_SSH_TARGET:-<user>@<host>} '$target_dir'"
exit 0
else
echo "RESULT: FAIL ($pass/$total) — Werkator bubblewrap builds are not usable on this host."
exit 1
fi