trails/infrastructure/brouter-host/download-segments.sh
Ullrich Schäfer c49047fd33
BRouter host compose + Planner auth + cd-brouter rewrite
Lands sections 3-5 of the relocate-brouter-to-dedicated-host change:
everything needed to run BRouter on the dedicated Hetzner Robot host
and have the Planner talk to it with the shared-secret header. Does
NOT flip the cutover — the flagship BRouter stays warm during soak.

## BRouter host compose (section 3)

New `infrastructure/brouter-host/` — a standalone compose project that
runs as the `trails` user on `ullrich.is`:

- `docker-compose.yml` — brouter + caddy sidecar. BRouter has no host
  port; caddy binds only to `10.0.1.10:17777` (vSwitch IP). Every
  service explicitly overrides the host's default Loki logging driver
  to `json-file` so logs don't leak to the operator's personal Loki.
- `Caddyfile` — single-purpose reverse proxy that requires
  `X-BRouter-Auth: ${BROUTER_AUTH_TOKEN}` on every request. `auto_https
  off` (vSwitch-only); default access log format omits request
  headers, so the token is never written to disk.
- `download-segments.sh` — crawls brouter.de, pulls planet-wide RD5
  tiles via `wget -N` (incremental). Idempotent, safe to cron.
- `README.md` — one-shot provisioning + token rotation + rollback
  notes.

`docker/brouter/Dockerfile` is patched to honor `JAVA_OPTS` (was
hardcoded `-Xmx1024M` in CMD). Default keeps the flagship's current
heap; compose on the dedicated host overrides to `-Xmx8g` for planet
scale on a 32 GB box.

## Planner shared-secret header (section 4)

`apps/planner/app/lib/brouter.ts`:

- Module-level guard: throws at startup in production if
  `BROUTER_AUTH_TOKEN` is unset.
- `authHeaders()` helper (reads env at call time, so tests can
  `vi.stubEnv` without module reset).
- Header attached on both `computeRoute` (per-segment) and
  `computeSegmentGpx`.

3 new unit tests cover header attachment + the no-token path.

`infrastructure/docker-compose.yml` passes `BROUTER_AUTH_TOKEN` to
the Planner service, and makes `BROUTER_URL` overridable via SOPS so
the cutover is a one-variable flip.

## cd-brouter workflow (section 5)

Rewritten to deploy to the dedicated host:

- SSH as `trails@${BROUTER_DEPLOY_HOST}` on port
  `${BROUTER_DEPLOY_SSH_PORT}` (2232) using
  `${BROUTER_DEPLOY_SSH_KEY}`.
- Decrypts SOPS, extracts ONLY `BROUTER_AUTH_TOKEN` into a `.env`
  file, scp'd alongside the compose project.
- `paths:` trigger now includes `infrastructure/brouter-host/**`.
- Segment download is NOT run here — first-time seed is a manual
  operator step (multi-hour). Routine re-runs are cron-able on the
  dedicated host.
- Grafana annotation step preserved (reaches flagship Grafana as
  before).

## What's NOT here

- `brouter:` service on the flagship is intentionally left in place
  (removed in section 7.5 after the 48 h soak window post-cutover).
- Observability (section 6) — Prometheus scrape + Loki shipping from
  the dedicated host — comes in a follow-up PR.
- Cutover itself (section 7) — flip `BROUTER_URL`, verify, remove the
  flagship brouter — is an operator action gated on first-time
  provisioning + smoke testing.

## Verification

`pnpm typecheck && pnpm lint && pnpm test` all clean; planner build
passes (the CI regression from #286 was fixed in #290).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:52:48 +02:00

80 lines
2.3 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Download / refresh BRouter RD5 segments from brouter.de for planet-wide
# coverage. Idempotent: re-running only fetches files that are new or
# updated upstream (wget -N uses Last-Modified). First run takes hours
# and pulls ~6080 GB; subsequent runs are cheap.
#
# Usage:
# ./download-segments.sh [dest_dir]
# dest_dir defaults to ./segments relative to this script
#
# Runs safely as non-root; no privileged operations. Can be cron'd.
#
# After a successful run, restart the brouter container so it reloads
# any updated segments:
# docker compose restart brouter
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEST_DIR="${1:-$SCRIPT_DIR/segments}"
BASE_URL="https://brouter.de/brouter/segments4"
mkdir -p "$DEST_DIR"
echo "Listing tiles at $BASE_URL/ ..."
# Extract RD5 filenames from the Apache-style directory listing.
# Pattern matches the standard brouter tile naming: W120_N40.rd5 etc.
tiles=$(curl --fail --silent --show-error --location "$BASE_URL/" \
| grep -oE '[WE][0-9]+_[NS][0-9]+\.rd5' \
| sort -u)
if [ -z "$tiles" ]; then
echo "ERROR: no tiles found at $BASE_URL/ (directory listing empty or blocked)" >&2
exit 1
fi
total=$(printf '%s\n' "$tiles" | wc -l | tr -d ' ')
echo "Found $total tiles. Destination: $DEST_DIR"
echo
cd "$DEST_DIR"
i=0
skipped=0
failed=0
while read -r tile; do
[ -z "$tile" ] && continue
i=$((i + 1))
# -N: only fetch if remote is newer than local (Last-Modified)
# -q: quiet; we print our own progress
if wget --no-verbose --timestamping --tries=3 --timeout=60 "$BASE_URL/$tile" 2>&1 | grep -q 'not retrieving'; then
skipped=$((skipped + 1))
fi
if [ ! -s "$tile" ]; then
echo " [$i/$total] FAILED: $tile"
failed=$((failed + 1))
fi
# Print heartbeat every 25 tiles so hour-long runs don't look hung
if [ $((i % 25)) -eq 0 ]; then
echo " [$i/$total] ... $skipped already-current, $failed failed so far"
fi
done <<< "$tiles"
echo
echo "Done. Totals:"
echo " attempted: $i"
echo " already current: $skipped"
echo " failed: $failed"
echo
echo "Destination size:"
du -sh "$DEST_DIR"
echo
echo "Tile count on disk:"
ls "$DEST_DIR"/*.rd5 2>/dev/null | wc -l
if [ "$failed" -gt 0 ]; then
echo
echo "WARNING: $failed downloads failed. Re-run the script to retry." >&2
exit 2
fi