arr fleet: FrankBook-Pro is the sole deployment, all egress VPN-gated

FrankBook-Pro becomes the only *arr/qBittorrent deployment. Every fleet
container (qB + five arrs) now shares gluetun's netns, so all egress —
torrent, indexer APIs, metadata lookups — exits the WireGuard tunnel by
construction (AllowedIPs=0.0.0.0/0); the arrs' own ports publish on the
gluetun unit, none on the arr units themselves.

- dankserver/compose.yaml: remove the six arr services (the Aug 20 compose
  copy — never healthy, superseded; config dirs trashed on the host)
- dankclaw/Caddyfile + shared/homepage/services.json: arr vhosts and
  probes repoint 192.168.0.235 -> 192.168.0.112 (FrankBook-Pro)
- frankbook-pro units: five arrs join gluetun netns (Wants/After/Network);
  gluetun publishes 9696/8989/7878/8686/8787 alongside 8080/6881
- tests/frankbook_quadlet.bats: new VPN egress seam — every arr+qB unit
  must share gluetun's netns and publish no port; gluetun must publish the
  whole fleet's ports
- deploy-plan.py: gluetun changes carry the whole netns family (a gluetun
  recreation kills all six joiners); dankserver compose set loses the arrs
- AGENTS.md: deployment is complete only when unit tests AND the Playwright
  E2E suite both pass
This commit is contained in:
Frank
2026-09-04 12:54:28 -07:00
parent 8a6362401a
commit 4c7465d00d
14 changed files with 101 additions and 163 deletions
+12
View File
@@ -130,6 +130,18 @@ Set `FRANK_SUDO_PW` for headless runs on a host without passwordless sudo
(remote sudo reads it from stdin via `sudo -S`). The value is never stored (remote sudo reads it from stdin via `sudo -S`). The value is never stored
in this repo. in this repo.
### Deployment completeness (MANDATORY)
A deployment is **not complete** when `deploy.sh` exits green. It is complete
only when BOTH of the following pass:
1. **Unit/config tests** — `./tests/run.sh` (all seams green), and
2. **Playwright end-to-end tests** — `tests/e2e/run.sh` drives the LIVE fleet
(homepage, Anubis/Authentik guard chain, guarded vhosts) in a real
browser. The E2E suite is deliberately excluded from `./tests/run.sh`
because it needs live hosts and credentials, so it MUST be invoked
explicitly after every behavior-affecting deploy.
```mermaid ```mermaid
graph LR graph LR
A[Edit locally] --> B[Tests pass] A[Edit locally] --> B[Tests pass]
+12 -12
View File
@@ -241,27 +241,27 @@ jelly.franksdank.cloud {
# --- *arr Stack (exempt by construction: own auth) --- # --- *arr Stack (exempt by construction: own auth) ---
prowlarr.franksdank.cloud { prowlarr.franksdank.cloud {
reverse_proxy 192.168.0.235:9696 reverse_proxy 192.168.0.112:9696
} }
radarr.franksdank.cloud { radarr.franksdank.cloud {
reverse_proxy 192.168.0.235:7878 reverse_proxy 192.168.0.112:7878
} }
sonarr.franksdank.cloud { sonarr.franksdank.cloud {
reverse_proxy 192.168.0.235:8989 reverse_proxy 192.168.0.112:8989
} }
lidarr.franksdank.cloud { lidarr.franksdank.cloud {
reverse_proxy 192.168.0.235:8686 reverse_proxy 192.168.0.112:8686
} }
readarr.franksdank.cloud { readarr.franksdank.cloud {
reverse_proxy 192.168.0.235:8787 reverse_proxy 192.168.0.112:8787
} }
qbittorrent.franksdank.cloud { qbittorrent.franksdank.cloud {
reverse_proxy 192.168.0.235:8080 reverse_proxy 192.168.0.112:8080
} }
# --- Internal listener: auth decision layer --------------------------------- # --- Internal listener: auth decision layer ---------------------------------
@@ -354,32 +354,32 @@ http://:8017 {
@prowlarr host prowlarr.franksdank.cloud @prowlarr host prowlarr.franksdank.cloud
handle @prowlarr { handle @prowlarr {
reverse_proxy 192.168.0.235:9696 reverse_proxy 192.168.0.112:9696
} }
@radarr host radarr.franksdank.cloud @radarr host radarr.franksdank.cloud
handle @radarr { handle @radarr {
reverse_proxy 192.168.0.235:7878 reverse_proxy 192.168.0.112:7878
} }
@sonarr host sonarr.franksdank.cloud @sonarr host sonarr.franksdank.cloud
handle @sonarr { handle @sonarr {
reverse_proxy 192.168.0.235:8989 reverse_proxy 192.168.0.112:8989
} }
@lidarr host lidarr.franksdank.cloud @lidarr host lidarr.franksdank.cloud
handle @lidarr { handle @lidarr {
reverse_proxy 192.168.0.235:8686 reverse_proxy 192.168.0.112:8686
} }
@readarr host readarr.franksdank.cloud @readarr host readarr.franksdank.cloud
handle @readarr { handle @readarr {
reverse_proxy 192.168.0.235:8787 reverse_proxy 192.168.0.112:8787
} }
@qbittorrent host qbittorrent.franksdank.cloud @qbittorrent host qbittorrent.franksdank.cloud
handle @qbittorrent { handle @qbittorrent {
reverse_proxy 192.168.0.235:8080 reverse_proxy 192.168.0.112:8080
} }
} }
-2
View File
@@ -20,5 +20,3 @@ COPAY_PASS=changeme
# Jellyfin # Jellyfin
JELLYFIN_PUBLISH_SERVER_URL=http://192.168.0.235:8096 JELLYFIN_PUBLISH_SERVER_URL=http://192.168.0.235:8096
# *arr Stack
ARR_TZ=America/New_York
-103
View File
@@ -146,109 +146,6 @@ services:
timeout: 10s timeout: 10s
retries: 3 retries: 3
# ═══════════════════════════════════════════════
# *arr Stack — automated media download
# ═══════════════════════════════════════════════
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=America/Los_Angeles
- UMASK_SET=022
volumes:
- /mnt/entropy/downloads:/downloads:ro
- /opt/prowlarr/config:/config
networks: [appnet]
ports:
- "9696:9696"
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=America/Los_Angeles
- UMASK_SET=022
volumes:
- /opt/qbittorrent/config:/config
- /mnt/entropy/downloads:/downloads
- /mnt/entropy/downloads/incomplete:/downloads/incomplete
ports:
- "8080:8080"
networks: [appnet]
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=America/Los_Angeles
- UMASK_SET=022
volumes:
- /opt/radarr/config:/config
- /mnt/entropy/Movies:/movies
- /mnt/entropy/downloads:/downloads
ports:
- "7878:7878"
networks: [appnet]
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=America/Los_Angeles
- UMASK_SET=022
volumes:
- /opt/sonarr/config:/config
- /mnt/entropy/TVShows:/tv
- /mnt/entropy/downloads:/downloads
ports:
- "8989:8989"
networks: [appnet]
lidarr:
image: lscr.io/linuxserver/lidarr:latest
container_name: lidarr
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=America/Los_Angeles
- UMASK_SET=022
volumes:
- /opt/lidarr/config:/config
- /mnt/entropy/Music:/music
- /mnt/entropy/downloads:/downloads
ports:
- "8686:8686"
networks: [appnet]
readarr:
image: lscr.io/linuxserver/readarr:amd64-1.0.980
container_name: readarr
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=America/Los_Angeles
- UMASK_SET=022
volumes:
- /opt/readarr/config:/config
- /mnt/entropy/eBooks:/books
- /mnt/entropy/downloads:/downloads
ports:
- "8787:8787"
networks: [appnet]
# ═══════════════════════════════════════════════ # ═══════════════════════════════════════════════
# Node Exporter — host metrics # Node Exporter — host metrics
@@ -5,11 +5,12 @@
# (installed by the frankbook-pro handler in scripts/deploy-remote.sh — # (installed by the frankbook-pro handler in scripts/deploy-remote.sh —
# install_quadlet targets the Linux hosts' systemctl --user, not the VM). # install_quadlet targets the Linux hosts' systemctl --user, not the VM).
# #
# WireGuard tunnel to ProtonVPN. qBittorrent shares this container's network # WireGuard tunnel to ProtonVPN. qBittorrent and all five *arrs share this
# namespace (Network=gluetun.container) so torrent egress never touches the # container's network namespace (Network=gluetun.container) so every fleet
# residential IP. qBittorrent's WebUI and BitTorrent ports publish HERE — a # container's egress — torrent traffic, indexer APIs, metadata lookups —
# netns-sharer publishes no ports of its own (same invariant as the dankclaw # exits ONLY through the tunnel (AllowedIPs=0.0.0.0/0). Every fleet port
# searxng/gluetun pair). Credentials live in # publishes HERE; a netns-sharer publishes no ports of its own (same
# invariant as the dankclaw searxng/gluetun pair). Credentials live in
# /home/core/Containers/gluetun/env inside the VM (mode 0600, never # /home/core/Containers/gluetun/env inside the VM (mode 0600, never
# committed); the private key is Mac-tunnel-specific and MUST NOT be copied # committed); the private key is Mac-tunnel-specific and MUST NOT be copied
# from dankclaw — one WireGuard peer key cannot serve two machines at once. # from dankclaw — one WireGuard peer key cannot serve two machines at once.
@@ -33,6 +34,11 @@ Environment=WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=${WIREGUARD_PERSISTENT_KEEPA
PublishPort=8080:8080 PublishPort=8080:8080
PublishPort=6881:6881 PublishPort=6881:6881
PublishPort=6881:6881/udp PublishPort=6881:6881/udp
PublishPort=9696:9696
PublishPort=8989:8989
PublishPort=7878:7878
PublishPort=8686:8686
PublishPort=8787:8787
[Service] [Service]
EnvironmentFile=/home/core/Containers/gluetun/env EnvironmentFile=/home/core/Containers/gluetun/env
@@ -3,10 +3,15 @@
# Install path is INSIDE the machine VM: # Install path is INSIDE the machine VM:
# /home/core/.config/containers/systemd/lidarr.container # /home/core/.config/containers/systemd/lidarr.container
# #
#
# Joins gluetun's network namespace: all egress exits through the WireGuard
# tunnel; 8686 publishes on the gluetun unit.
# Grab-only: no media binds; config migrated from dankserver's retired # Grab-only: no media binds; config migrated from dankserver's retired
# bare container. # bare container.
[Unit] [Unit]
Description=Lidarr (FrankBook-Pro) Description=Lidarr (FrankBook-Pro)
Wants=gluetun.service
After=gluetun.service
[Container] [Container]
ContainerName=lidarr ContainerName=lidarr
@@ -16,7 +21,7 @@ Environment=PGID=0
Environment=TZ=America/Los_Angeles Environment=TZ=America/Los_Angeles
Environment=UMASK_SET=022 Environment=UMASK_SET=022
Volume=/Volumes/COMPLETE/lidarr-data:/config Volume=/Volumes/COMPLETE/lidarr-data:/config
PublishPort=8686:8686 Network=gluetun.container
[Service] [Service]
TimeoutStartSec=900 TimeoutStartSec=900
@@ -3,6 +3,10 @@
# Install path is INSIDE the machine VM: # Install path is INSIDE the machine VM:
# /home/core/.config/containers/systemd/prowlarr.container # /home/core/.config/containers/systemd/prowlarr.container
# #
#
# Joins gluetun's network namespace: all egress (indexer APIs, metadata
# lookups) exits through the WireGuard tunnel; 9696 publishes on the
# gluetun unit. Same-netns targets (qB, the arrs) are loopback addresses.
# Arr on the Mac is grab-only: indexer + app-sync management lives here, the # Arr on the Mac is grab-only: indexer + app-sync management lives here, the
# arrs never auto-import media (the user reviews completed torrents in # arrs never auto-import media (the user reviews completed torrents in
# /Volumes/COMPLETE/"Finished Torrents" and moves files manually into the # /Volumes/COMPLETE/"Finished Torrents" and moves files manually into the
@@ -11,9 +15,12 @@
# Config is migrated from dankserver's retired bare container # Config is migrated from dankserver's retired bare container
# (tar-archived first, per the Phase 4 cutover note) — indexers and app # (tar-archived first, per the Phase 4 cutover note) — indexers and app
# settings survive; stale host pointers (old qbit at 192.168.0.235:8080) # settings survive; stale host pointers (old qbit at 192.168.0.235:8080)
# are re-pointed to the Mac's qbit via its API after first boot. # are re-pointed to loopback qBittorrent (127.0.0.1:8080, same netns) via
# its API after first boot.
[Unit] [Unit]
Description=Prowlarr (FrankBook-Pro) Description=Prowlarr (FrankBook-Pro)
Wants=gluetun.service
After=gluetun.service
[Container] [Container]
ContainerName=prowlarr ContainerName=prowlarr
@@ -23,7 +30,7 @@ Environment=PGID=0
Environment=TZ=America/Los_Angeles Environment=TZ=America/Los_Angeles
Environment=UMASK_SET=022 Environment=UMASK_SET=022
Volume=/Volumes/COMPLETE/prowlarr-data:/config Volume=/Volumes/COMPLETE/prowlarr-data:/config
PublishPort=9696:9696 Network=gluetun.container
[Service] [Service]
TimeoutStartSec=900 TimeoutStartSec=900
@@ -3,10 +3,15 @@
# Install path is INSIDE the machine VM: # Install path is INSIDE the machine VM:
# /home/core/.config/containers/systemd/radarr.container # /home/core/.config/containers/systemd/radarr.container
# #
#
# Joins gluetun's network namespace: all egress exits through the WireGuard
# tunnel; 7878 publishes on the gluetun unit.
# Grab-only: no media binds; config migrated from dankserver's retired # Grab-only: no media binds; config migrated from dankserver's retired
# bare container. # bare container.
[Unit] [Unit]
Description=Radarr (FrankBook-Pro) Description=Radarr (FrankBook-Pro)
Wants=gluetun.service
After=gluetun.service
[Container] [Container]
ContainerName=radarr ContainerName=radarr
@@ -16,7 +21,7 @@ Environment=PGID=0
Environment=TZ=America/Los_Angeles Environment=TZ=America/Los_Angeles
Environment=UMASK_SET=022 Environment=UMASK_SET=022
Volume=/Volumes/COMPLETE/radarr-data:/config Volume=/Volumes/COMPLETE/radarr-data:/config
PublishPort=7878:7878 Network=gluetun.container
[Service] [Service]
TimeoutStartSec=900 TimeoutStartSec=900
@@ -3,6 +3,9 @@
# Install path is INSIDE the machine VM: # Install path is INSIDE the machine VM:
# /home/core/.config/containers/systemd/readarr.container # /home/core/.config/containers/systemd/readarr.container
# #
#
# Joins gluetun's network namespace: all egress exits through the WireGuard
# tunnel; 8787 publishes on the gluetun unit.
# Grab-only: no media binds; config migrated from dankserver's retired # Grab-only: no media binds; config migrated from dankserver's retired
# bare container. # bare container.
# #
@@ -11,6 +14,8 @@
# of a broken 1.x nightly for Readarr; the pin avoids surprises. # of a broken 1.x nightly for Readarr; the pin avoids surprises.
[Unit] [Unit]
Description=Readarr (FrankBook-Pro) Description=Readarr (FrankBook-Pro)
Wants=gluetun.service
After=gluetun.service
[Container] [Container]
ContainerName=readarr ContainerName=readarr
@@ -20,7 +25,7 @@ Environment=PGID=0
Environment=TZ=America/Los_Angeles Environment=TZ=America/Los_Angeles
Environment=UMASK_SET=022 Environment=UMASK_SET=022
Volume=/Volumes/COMPLETE/readarr-data:/config Volume=/Volumes/COMPLETE/readarr-data:/config
PublishPort=8787:8787 Network=gluetun.container
[Service] [Service]
TimeoutStartSec=900 TimeoutStartSec=900
@@ -3,11 +3,16 @@
# Install path is INSIDE the machine VM: # Install path is INSIDE the machine VM:
# /home/core/.config/containers/systemd/sonarr.container # /home/core/.config/containers/systemd/sonarr.container
# #
#
# Joins gluetun's network namespace: all egress exits through the WireGuard
# tunnel; 8989 publishes on the gluetun unit.
# Grab-only: no media binds (the TV library lives on dankserver's entropy # Grab-only: no media binds (the TV library lives on dankserver's entropy
# disk, which the Mac never mounts); config migrated from dankserver's # disk, which the Mac never mounts); config migrated from dankserver's
# retired bare container. # retired bare container.
[Unit] [Unit]
Description=Sonarr (FrankBook-Pro) Description=Sonarr (FrankBook-Pro)
Wants=gluetun.service
After=gluetun.service
[Container] [Container]
ContainerName=sonarr ContainerName=sonarr
@@ -17,7 +22,7 @@ Environment=PGID=0
Environment=TZ=America/Los_Angeles Environment=TZ=America/Los_Angeles
Environment=UMASK_SET=022 Environment=UMASK_SET=022
Volume=/Volumes/COMPLETE/sonarr-data:/config Volume=/Volumes/COMPLETE/sonarr-data:/config
PublishPort=8989:8989 Network=gluetun.container
[Service] [Service]
TimeoutStartSec=900 TimeoutStartSec=900
+5 -9
View File
@@ -147,12 +147,6 @@ DANKSERVER_COMPOSE_SERVICES = {
"copyparty", "copyparty",
"jellyfin", "jellyfin",
"calibre-web", "calibre-web",
"prowlarr",
"qbittorrent",
"radarr",
"sonarr",
"lidarr",
"readarr",
} }
COMPOSE_NAME_MAP = {"mariadb": "romm-mariadb", "redis": "romm-redis"} COMPOSE_NAME_MAP = {"mariadb": "romm-mariadb", "redis": "romm-redis"}
@@ -341,9 +335,11 @@ def services_for_path(path: str, target: str) -> set[str]:
if len(parts) >= 3 and parts[2] == "quadlet": if len(parts) >= 3 and parts[2] == "quadlet":
name = parts[3].removesuffix(".container") name = parts[3].removesuffix(".container")
if name == "gluetun": if name == "gluetun":
# qBittorrent shares gluetun's network namespace; a gluetun # Every *arr + qBittorrent container shares gluetun's
# recreation requires both units to be restarted. # network namespace; a gluetun recreation kills all of
_add(services, "gluetun", "qbittorrent") # them, so the plan restarts the whole netns family.
_add(services, "gluetun", "qbittorrent",
"prowlarr", "sonarr", "radarr", "lidarr", "readarr")
elif name in FRANKBOOK_SERVICES: elif name in FRANKBOOK_SERVICES:
_add(services, name) _add(services, name)
return services return services
+6 -6
View File
@@ -104,7 +104,7 @@
"description": "TV series library and automation.", "description": "TV series library and automation.",
"url": "http://sonarr.franksdank.cloud", "url": "http://sonarr.franksdank.cloud",
"docs": "", "docs": "",
"probe": "192.168.0.235:8989", "probe": "192.168.0.112:8989",
"method": "http", "method": "http",
"wip": false, "wip": false,
"showLink": true "showLink": true
@@ -259,7 +259,7 @@
"description": "Indexer manager.", "description": "Indexer manager.",
"url": "http://prowlarr.franksdank.cloud", "url": "http://prowlarr.franksdank.cloud",
"docs": "", "docs": "",
"probe": "192.168.0.235:9696", "probe": "192.168.0.112:9696",
"method": "http", "method": "http",
"wip": false, "wip": false,
"showLink": true "showLink": true
@@ -270,7 +270,7 @@
"description": "Movie library and automation.", "description": "Movie library and automation.",
"url": "http://radarr.franksdank.cloud", "url": "http://radarr.franksdank.cloud",
"docs": "", "docs": "",
"probe": "192.168.0.235:7878", "probe": "192.168.0.112:7878",
"method": "http", "method": "http",
"wip": false, "wip": false,
"showLink": true "showLink": true
@@ -281,7 +281,7 @@
"description": "Book library and automation.", "description": "Book library and automation.",
"url": "http://readarr.franksdank.cloud", "url": "http://readarr.franksdank.cloud",
"docs": "", "docs": "",
"probe": "192.168.0.235:8787", "probe": "192.168.0.112:8787",
"method": "http", "method": "http",
"wip": false, "wip": false,
"showLink": true "showLink": true
@@ -292,7 +292,7 @@
"description": "Music library and automation.", "description": "Music library and automation.",
"url": "http://lidarr.franksdank.cloud", "url": "http://lidarr.franksdank.cloud",
"docs": "", "docs": "",
"probe": "192.168.0.235:8686", "probe": "192.168.0.112:8686",
"method": "http", "method": "http",
"wip": false, "wip": false,
"showLink": true "showLink": true
@@ -303,7 +303,7 @@
"description": "Torrent client.", "description": "Torrent client.",
"url": "http://qbittorrent.franksdank.cloud", "url": "http://qbittorrent.franksdank.cloud",
"docs": "", "docs": "",
"probe": "192.168.0.235:8080", "probe": "192.168.0.112:8080",
"method": "http", "method": "http",
"wip": false, "wip": false,
"showLink": true "showLink": true
+2 -2
View File
@@ -111,11 +111,11 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
[ "$output" = $'gitea-postgres\ngitea-server' ] [ "$output" = $'gitea-postgres\ngitea-server' ]
} }
@test "frankbook-pro gluetun changes carry qbittorrent; arr changes stay scoped" { @test "frankbook-pro gluetun changes carry the whole netns family; arr changes stay scoped" {
run python3 "$PLANNER" --target frankbook-pro --paths \ run python3 "$PLANNER" --target frankbook-pro --paths \
frankbook-pro/gluetun/quadlet/gluetun.container frankbook-pro/gluetun/quadlet/gluetun.container
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ "$output" = $'gluetun\nqbittorrent' ] [ "$output" = $'gluetun\nprowlarr\nqbittorrent\nradarr\nsonarr\nlidarr\nreadarr' ]
run python3 "$PLANNER" --target frankbook-pro --paths \ run python3 "$PLANNER" --target frankbook-pro --paths \
frankbook-pro/sonarr/quadlet/sonarr.container frankbook-pro/sonarr/quadlet/sonarr.container
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
+20 -18
View File
@@ -2,9 +2,12 @@
# Seam: the FrankBook-Pro quadlet units (gluetun, qbittorrent, five *arrs). # Seam: the FrankBook-Pro quadlet units (gluetun, qbittorrent, five *arrs).
# These units are installed INSIDE the Mac's podman machine VM by the # These units are installed INSIDE the Mac's podman machine VM by the
# frankbook-pro handler in deploy-remote.sh. The contract pinned here: # frankbook-pro handler in deploy-remote.sh. The contract pinned here:
# boot persistence, self-healing, the gluetun netns invariant (qBittorrent # boot persistence, self-healing, the VPN egress invariant (the whole
# publishes no port of its own; gluetun publishes both), the verbatim # *arr fleet + qBittorrent share gluetun's netns and publish no ports of
# plan-locked data-root mounts, and zero literal secrets in tracked files. # 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 load helpers
@@ -31,18 +34,22 @@ FRANKBOOK_UNITS=(
done done
} }
@test "qbittorrent joins gluetun's netns and publishes no port of its own" { @test "every arr and qbittorrent joins gluetun's netns and publishes no port" {
local unit="$REPO_ROOT/frankbook-pro/qbittorrent/quadlet/qbittorrent.container" local svc unit
grep -q '^Network=gluetun.container$' "$unit" || return 1 for svc in qbittorrent prowlarr sonarr radarr lidarr readarr; do
grep -q '^Wants=gluetun.service$' "$unit" || return 1 unit="$REPO_ROOT/frankbook-pro/$svc/quadlet/$svc.container"
grep -q '^After=gluetun.service$' "$unit" || return 1 grep -q '^Network=gluetun.container$' "$unit" || return 1
! grep -q '^PublishPort=' "$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 qbittorrent's WebUI and torrent ports and needs NET_ADMIN+tun" { @test "gluetun publishes the whole fleet's ports and needs NET_ADMIN+tun" {
local unit="$REPO_ROOT/frankbook-pro/gluetun/quadlet/gluetun.container" local unit="$REPO_ROOT/frankbook-pro/gluetun/quadlet/gluetun.container"
grep -q '^PublishPort=8080:8080$' "$unit" || return 1 for port in 8080 6881 9696 8989 7878 8686 8787; do
grep -q '^PublishPort=6881:6881$' "$unit" || return 1 grep -q "^PublishPort=$port:$port\$" "$unit" || return 1
done
grep -q '^PublishPort=6881:6881/udp$' "$unit" || return 1 grep -q '^PublishPort=6881:6881/udp$' "$unit" || return 1
grep -q '^AddCapability=NET_ADMIN$' "$unit" || return 1 grep -q '^AddCapability=NET_ADMIN$' "$unit" || return 1
grep -q '^AddDevice=/dev/net/tun:/dev/net/tun$' "$unit" || return 1 grep -q '^AddDevice=/dev/net/tun:/dev/net/tun$' "$unit" || return 1
@@ -69,12 +76,7 @@ FRANKBOOK_UNITS=(
grep -q '^Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE$' "$unit" || return 1 grep -q '^Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE$' "$unit" || return 1
} }
@test "every arr serves its standard port and readarr keeps its pinned image" { @test "readarr keeps its pinned image" {
grep -q '^PublishPort=9696:9696$' "$REPO_ROOT/frankbook-pro/prowlarr/quadlet/prowlarr.container" || return 1
grep -q '^PublishPort=8989:8989$' "$REPO_ROOT/frankbook-pro/sonarr/quadlet/sonarr.container" || return 1
grep -q '^PublishPort=7878:7878$' "$REPO_ROOT/frankbook-pro/radarr/quadlet/radarr.container" || return 1
grep -q '^PublishPort=8686:8686$' "$REPO_ROOT/frankbook-pro/lidarr/quadlet/lidarr.container" || return 1
grep -q '^PublishPort=8787:8787$' "$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
grep -q '^Image=lscr.io/linuxserver/readarr:amd64-1.0.980$' \ grep -q '^Image=lscr.io/linuxserver/readarr:amd64-1.0.980$' \
"$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1 "$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
} }