Files
serverAppConfigs/tests/frankbook_quadlet.bats
T
Frank d2810ab610 feat(security): digest-pin every registry image on all three hosts (fleet law)
Closing the command-center security queue #70/#71/#72/#73/#74: rolling tags
let a rebuild move under a running service with nobody approving it, and
make a CVE audit ungradeable. 36 quadlet units across dankclaw, dankserver
and frankbook-pro now carry tag@sha256; localhost/* builds stay exempt
(their build is the pin). The pihole and copyparty pins pull the fixed
builds; valkey 9.1.2 (already running) and redis 7.4.11 are pinned in
place, vendor fixes TBD.

New gate tests/quadlet_image_pins.bats: no unpinned registry image, no
generator-rejected key anywhere. Three seams that pinned the old exact-tag
law (frankbook readarr, open_webui, seerr) now require tag AND digest -
strictly stronger. Quadlet Lifecycle docs: batch-abort law, entrypoint
flags-only law, pin law.
2026-10-21 16:32:36 -07:00

84 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bats
# Seam: the FrankBook-Pro quadlet units (gluetun, qbittorrent, five *arrs).
# These units are installed INSIDE the Mac's podman machine VM by the
# frankbook-pro handler in deploy-remote.sh. The contract pinned here:
# boot persistence, self-healing, the VPN egress invariant (the whole
# *arr fleet + qBittorrent share gluetun's netns and publish no ports of
# their own, so ALL egress exits through the WireGuard tunnel —
# AllowedIPs=0.0.0.0/0 — by construction; gluetun publishes every fleet
# port), the verbatim plan-locked data-root mounts, and zero literal
# secrets in tracked files.
load helpers
FRANKBOOK_UNITS=(
gluetun/quadlet/gluetun
qbittorrent/quadlet/qbittorrent
prowlarr/quadlet/prowlarr
sonarr/quadlet/sonarr
radarr/quadlet/radarr
lidarr/quadlet/lidarr
readarr/quadlet/readarr
)
@test "all seven frankbook-pro units exist under frankbook-pro/<svc>/quadlet/" {
for unit in "${FRANKBOOK_UNITS[@]}"; do
[ -f "$REPO_ROOT/frankbook-pro/$unit.container" ] || return 1
done
}
@test "every frankbook-pro unit is boot-persistent and self-healing" {
for unit in "${FRANKBOOK_UNITS[@]}"; do
grep -q '^WantedBy=default.target$' "$REPO_ROOT/frankbook-pro/$unit.container" || return 1
grep -q '^Restart=always$' "$REPO_ROOT/frankbook-pro/$unit.container" || return 1
done
}
@test "every arr and qbittorrent joins gluetun's netns and publishes no port" {
local svc unit
for svc in qbittorrent prowlarr sonarr radarr lidarr readarr; do
unit="$REPO_ROOT/frankbook-pro/$svc/quadlet/$svc.container"
grep -q '^Network=gluetun.container$' "$unit" || return 1
grep -q '^Wants=gluetun.service$' "$unit" || return 1
grep -q '^After=gluetun.service$' "$unit" || return 1
! grep -q '^PublishPort=' "$unit" || return 1
done
}
@test "gluetun publishes the whole fleet's ports and needs NET_ADMIN+tun" {
local unit="$REPO_ROOT/frankbook-pro/gluetun/quadlet/gluetun.container"
for port in 8080 6881 9696 8989 7878 8686 8787; do
grep -q "^PublishPort=$port:$port\$" "$unit" || return 1
done
grep -q '^PublishPort=6881:6881/udp$' "$unit" || return 1
grep -q '^AddCapability=NET_ADMIN$' "$unit" || return 1
grep -q '^AddDevice=/dev/net/tun:/dev/net/tun$' "$unit" || return 1
}
@test "gluetun credentials come from a machine-local env file, never literals" {
local unit="$REPO_ROOT/frankbook-pro/gluetun/quadlet/gluetun.container"
grep -q '^EnvironmentFile=/home/core/Containers/gluetun/env$' "$unit" || return 1
# Only ${VAR} expansions may carry the WireGuard material.
if grep -qE '^Environment=WIREGUARD_PRIVATE_KEY=[^$]' "$unit"; then
return 1
fi
}
@test "data roots follow the plan-locked virtiofs layout" {
for svc in qbittorrent prowlarr sonarr radarr lidarr readarr; do
grep -q "^Volume=/Volumes/COMPLETE/$svc-data:/config$" \
"$REPO_ROOT/frankbook-pro/$svc/quadlet/$svc.container" || return 1
done
# qBittorrent additionally mounts both external disks verbatim: the
# seeded conf references the literal /Volumes paths.
local unit="$REPO_ROOT/frankbook-pro/qbittorrent/quadlet/qbittorrent.container"
grep -q '^Volume=/Volumes/COMPLETE:/Volumes/COMPLETE$' "$unit" || return 1
grep -q '^Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE$' "$unit" || return 1
}
@test "readarr keeps its pinned image" {
# 2026-10-21 fleet law: version tag AND digest (tests/quadlet_image_pins.bats)
grep -qE '^Image=lscr\.io/linuxserver/readarr:amd64-1\.0\.980@sha256:[0-9a-f]{64}$' \
"$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
}