fix(deploy): put ingress on the proxy, and make the lint script runnable

Three artefacts disagreed about where nginx runs. design.md §6.2 and the vhost
both said the hanzalova proxy; the API bound 127.0.0.1 and the workflow rsynced
the dashboard to bob. That combination deploys green and then serves nothing,
since a proxy on another host cannot reach bob's loopback.

Resolve it the way design.md already stated: nginx on the proxy, dashboard
shipped there, API bound 0.0.0.0 behind firewalld and the mesh. The health probe
now runs from the proxy over the mesh rather than from bob's loopback, so it
fails when firewalld is closed instead of passing regardless. infra-setup.sh
grows a proxy grant scoped to static files alone, and the nginx vhost install as
a manual step — it needs a certificate, and nothing was telling the operator to
install it at all.

npm run lint had never run: eslint 9 needs a flat config and there was none. Add
it, ignoring the ts-rs generated bindings, and run it in CI so it stays true.

Untrack dashboard/tsconfig.tsbuildinfo, a build artifact that would have put a
spurious diff in every pull request tireless opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
This commit is contained in:
rob thijssen
2026-08-07 15:36:54 +03:00
parent ddb2574bbe
commit 835e3e98f2
7 changed files with 135 additions and 10 deletions

View File

@@ -15,6 +15,11 @@ set -euo pipefail
APP=tireless
API_HOST="${API_HOST:-bob.hanzalova.internal}"
API_PORT="${API_PORT:-23296}"
# Ingress runs on the office proxy, not on bob (doc/plan/design.md §6.2). The
# dashboard is served from there and /v1 is reverse-proxied across the mesh, so
# the proxy needs its own (much smaller) deploy grant.
WEB_HOST="${WEB_HOST:-hanzalova.internal}"
WEB_ROOT="${WEB_ROOT:-/var/www/tireless}"
RUNNER_PUBKEY="${RUNNER_PUBKEY:-$HOME/.ssh/id_gitea_ci.pub}"
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
@@ -96,7 +101,39 @@ SUDOERS
provision_host "$API_HOST"
# ---------------------------------------------------------------------------
# 3. Manual steps that cannot be automated
# 3. Proxy host: web root and a deploy grant scoped to it alone
# ---------------------------------------------------------------------------
provision_web_host() {
local host="$1"
info "provisioning ingress on $host"
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$host" true; then
warn "$host unreachable; skipping (re-run once it is back)"
return 0
fi
ssh "$host" 'sudo useradd --system --create-home --home-dir /var/lib/gitea_ci \
--shell /usr/sbin/nologin gitea_ci || echo "gitea_ci already exists"'
ssh "$host" 'sudo install -d -o gitea_ci -g gitea_ci -m 0700 /var/lib/gitea_ci/.ssh'
rsync --rsync-path 'sudo rsync' --chown gitea_ci:gitea_ci --chmod 0600 \
"$RUNNER_PUBKEY" "$host:/var/lib/gitea_ci/.ssh/authorized_keys"
ssh "$host" "sudo install -d -o root -g root -m 0755 $WEB_ROOT"
# Deliberately narrower than the API host's grant: the proxy only ever
# receives static files. It gets no systemctl, no binaries, no config.
ssh "$host" "sudo tee /etc/sudoers.d/${APP}_web_gitea_ci >/dev/null" <<SUDOERS
gitea_ci ALL=(root) NOPASSWD: /usr/bin/rsync * ${WEB_ROOT}/
gitea_ci ALL=(root) NOPASSWD: /usr/sbin/restorecon -R ${WEB_ROOT}
SUDOERS
ssh "$host" "sudo visudo -cf /etc/sudoers.d/${APP}_web_gitea_ci"
info "$host ingress provisioned"
}
provision_web_host "$WEB_HOST"
# ---------------------------------------------------------------------------
# 4. Manual steps that cannot be automated
# ---------------------------------------------------------------------------
cat <<'MANUAL'
@@ -129,7 +166,21 @@ Remaining one-time steps (operator, on the target host):
The protection rule is what keeps an unattended agent from writing to main.
Verify it rather than assuming it.
3. Postgres role and ident mapping (architecture/generic.md §5).
3. nginx vhost on the proxy.
The deploy ships the built dashboard to the proxy's web root but does not
install the vhost — that is one-time, and it needs a certificate:
- mint the per-service cert for tireless.internal per
architecture/internal-tls.md;
- copy asset/nginx/tireless.hanzalova.conf to the proxy's conf.d;
- sudo nginx -t && sudo systemctl reload nginx
The vhost serves the dashboard locally and reverse-proxies /v1 to
bob:23296 across the mesh. That is why the API binds 0.0.0.0 rather than
loopback, and why asset/firewalld/ opens the port. Those three facts are one
decision — if you move ingress onto bob, change all three together.
4. Postgres role and ident mapping (architecture/generic.md §5).
On magrathea AND frankie:
- create role `tireless_rw`, and a `tireless` database;
- drop /var/lib/pgsql/18/data/pg_ident.conf.d/<this-host-fqdn>.conf