Files
serverAppConfigs/tests/marreta_quadlet.bats
T
Frank 65bd5ccea7 feat(rank6): node-exporter renders from a template; guard chain is one snippet
- service-map render node-exporter-unit --host H: the MIRROR pair comment
  ('keep this file in step') becomes a template. Per-host title, LAN
  address from the hosts map, port from the node-<host> scrape job. Both
  tracked quadlets are now rendered artifacts; check_drift gates them.
- dankclaw/Caddyfile: the nine verbatim Anubis pre-auth chains
  (reverse_proxy anubis:8923 + X-Real-Ip) collapse into the (guarded)
  named snippet; guarded vhosts are 'import guarded'. Validated with real
  caddy 2.11.7 (import resolution clean; the stock image stops at the
  porkbun module and the git basicauth placeholder, as designed).
- vhost-scoped guard greps (authentik, homepage_caddy, marreta_quadlet)
  now pin 'import guarded' in the vhost block.
- Service Inventory + Authentik docs carry the new laws.
2026-10-13 22:40:50 -07:00

139 lines
5.8 KiB
Bash
Executable File

#!/usr/bin/env bats
# Seam: Marreta content-cleaning proxy boot-persistence + egress + guard chain.
#
# Marreta (Laravel / serversideup fpm-nginx) and its Lightpanda browser
# companion run as rootless Quadlet units that share gluetun's network
# namespace, so every outbound fetch — the app's curl strategy AND the
# browser's own requests — exits the same WireGuard tunnel as SearXNG.
# The load-bearing invariants this seam defends:
# * netns sharing (the egress contract)
# * marreta listens on 8085, NOT 8080 (SearXNG already owns 8080 in the
# shared netns; a collision would fail the container at bind time)
# * gluetun publishes host 8082 -> 8085 (the ingress contract Caddy dials)
# * the browser is reached on loopback (same netns), never published
# * the guard chain: public vhost -> Anubis, post-auth listener -> 8082
# * boot persistence + self-heal + no leaked secrets
load helpers
QDIR="$REPO_ROOT/dankclaw/marreta/quadlet"
MARRETA="$QDIR/marreta.container"
BROWSER="$QDIR/marreta-browser.container"
GLUETUN="$REPO_ROOT/dankclaw/searxng/quadlet/gluetun.container"
@test "both marreta quadlet files exist" {
[ -f "$MARRETA" ]
[ -f "$BROWSER" ]
}
@test "marreta shares gluetun's network namespace (egress exits the tunnel)" {
grep -qE '^Network=gluetun\.container' "$MARRETA" \
|| { echo "marreta.container must set Network=gluetun.container"; return 1; }
}
@test "the browser companion shares gluetun's network namespace too" {
grep -qE '^Network=gluetun\.container' "$BROWSER" \
|| { echo "marreta-browser.container must set Network=gluetun.container"; return 1; }
}
@test "marreta relocates off SearXNG's 8080 to NGINX_HTTP_PORT=8085" {
# The shared netns already has SearXNG on 8080. serversideup fpm-nginx
# envsubst's the site config from ${NGINX_HTTP_PORT} at container start,
# so this is what actually moves the listen port off the collision.
grep -qE '^Environment=NGINX_HTTP_PORT=8085$' "$MARRETA" \
|| { echo "marreta.container must set Environment=NGINX_HTTP_PORT=8085"; return 1; }
}
@test "marreta publishes no port of its own (the port lives on gluetun)" {
if grep -qE '^PublishPort=' "$MARRETA"; then
echo "marreta.container must not PublishPort; the netns owner (gluetun) publishes it"
return 1
fi
return 0
}
@test "gluetun publishes marreta on 8082:8085" {
grep -qE '^PublishPort=8082:8085' "$GLUETUN" \
|| { echo "gluetun.container must publish 8082:8085 for marreta"; return 1; }
}
@test "marreta reaches the browser on loopback (same netns), not by service name" {
grep -qE '^Environment=BROWSER_WS_ENDPOINT=ws://127\.0\.0\.1:9222$' "$MARRETA" \
|| { echo "marreta.container must point BROWSER_WS_ENDPOINT at ws://127.0.0.1:9222"; return 1; }
}
@test "the browser companion publishes no port (loopback-only CDP)" {
if grep -qE '^PublishPort=' "$BROWSER"; then
echo "marreta-browser.container must not PublishPort; CDP is loopback-only"
return 1
fi
return 0
}
@test "marreta feeds \${VAR} expansion via [Service] EnvironmentFile" {
# An EnvironmentFile in [Container] lands in the ignored [X-Container]
# section and does NOT feed ${VAR} expansion. It must be in [Service].
awk '
/^\[Service\]/ { svc=1 }
/^\[Container\]/ { svc=0 }
/^EnvironmentFile=/ { if (svc) found=1 }
END { exit found ? 0 : 1 }
' "$MARRETA" || { echo "marreta.container needs EnvironmentFile in [Service]"; return 1; }
}
@test "marreta maps the seeded secrets to the app's own env names" {
grep -qE '^Environment=APP_KEY=\$\{MARRETA_APP_KEY\}$' "$MARRETA"
grep -qE '^Environment=ADMIN_EMAIL=\$\{MARRETA_ADMIN_EMAIL\}$' "$MARRETA"
grep -qE '^Environment=ADMIN_PASSWORD=\$\{MARRETA_ADMIN_PASSWORD\}$' "$MARRETA"
}
@test "both units are installed for boot (WantedBy=default.target)" {
grep -q 'WantedBy=default.target' "$MARRETA"
grep -q 'WantedBy=default.target' "$BROWSER"
}
@test "both units self-heal (Restart=always)" {
grep -q '^Restart=always' "$MARRETA"
grep -q '^Restart=always' "$BROWSER"
}
@test "marreta orders after gluetun and the browser it depends on" {
grep -qE '^After=.*gluetun\.service' "$MARRETA"
grep -qE '^After=.*marreta-browser\.service' "$MARRETA"
}
@test "no secret material in the marreta quadlet files" {
# The units reference \${VAR} placeholders, never literal values.
has_no_leaked_secret "$MARRETA"
has_no_leaked_secret "$BROWSER"
}
@test "the Caddyfile guards the marreta vhost through Anubis" {
# Public edge: the vhost must proxy to the challenge proxy, not straight to the app.
awk '
/^marreta\.franksdank\.cloud \{/ { inblk=1 }
inblk && /import guarded/ { challenge=1 }
inblk && /^\}/ { inblk=0 }
END { exit challenge ? 0 : 1 }
' "$REPO_ROOT/dankclaw/Caddyfile" \
|| { echo "marreta vhost must import the guarded chain (the one named Anubis snippet)"; return 1; }
}
@test "the Caddyfile post-auth listener routes marreta to the published 8082" {
# The end of the guard chain: authenticated traffic returns to :8017 and
# the Host-matched handler must dial the gluetun-published marreta port.
awk '
/^http:\/\/:8017 \{/ { inlis=1 }
inlis && /@marreta host marreta\.franksdank\.cloud/ { matched=1 }
inlis && matched && /reverse_proxy 192\.168\.0\.143:8082/ { routed=1 }
END { exit routed ? 0 : 1 }
' "$REPO_ROOT/dankclaw/Caddyfile" \
|| { echo "post-auth listener must route @marreta to 192.168.0.143:8082"; return 1; }
}
@test "the deployer has a marreta handler that seeds env and installs both units" {
grep -q 'deploy_marreta()' "$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'seed_marreta_env()' "$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'marreta) deploy_marreta ;;' "$REPO_ROOT/scripts/deploy-remote.sh"
}