diff --git a/dankclaw/.env.example b/dankclaw/.env.example index efa5081..c2b4c8e 100644 --- a/dankclaw/.env.example +++ b/dankclaw/.env.example @@ -3,7 +3,7 @@ # SearXNG SEARXNG_SECRET=changeme -SEARXNG_BASE_URL=http://search.dankclaw.local/ +SEARXNG_BASE_URL=http://search.franksdank.cloud/ # Gluetun VPN (see https://github.com/qdm12/gluetun-wiki) # Single-peer WireGuard model. Fill from your provider's WireGuard config. @@ -24,10 +24,10 @@ GITEA_DB_PASSWORD=changeme GRAFANA_ADMIN_USER=admin GRAFANA_ADMIN_PASSWORD=changeme -# OpenWebUI -> llama.cpp on Dankserver (frank-default-large, port 7770) -# 2026-08-17 redo: the llama.cpp box moved from Dankclaw to Dankserver. -OPENAI_API_BASE_URL=http://192.168.0.235:7770/v1 -OPENAI_API_KEY=not-needed +# Caddy (the homelab's single reverse proxy, on this host) +# bcrypt hash of the homepage/protected-vhost password (caddy hash-password). +# deploy.sh renders it into /home/dank/Containers/caddy/Caddyfile. +CADDY_BASICAUTH_HASH=changeme # Hermes -> Signal (signal-cli daemon on the host, port 8080) # E.164 number of the linked Signal account (your primary number, for Note-to-Self). diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index 81733ea..2362271 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -1,12 +1,19 @@ -# Dankclaw Caddyfile -# Reverse proxy for the Dankclaw stack. Caddy runs as a container on the -# podman network; upstreams are container names. Homepage is password -# protected. Replace the bcrypt hashes below (caddy hash-password) at deploy. +# Homelab Caddyfile — the single Caddy for both hosts. # -# Naming: flat franksdank.cloud scheme (see docs/Infrastructure/Domain-DNS.md). +# Caddy runs as a container on dankclaw (192.168.0.143, this repo's +# compose.yaml service 'caddy') and fronts every service on both hosts: +# - dankclaw services: by container name (compose network) or by the +# published LAN port (quadlet services on isolated networks) +# - dankserver services (192.168.0.235): by published LAN port +# +# Naming: flat franksdank.cloud scheme (see docs/Infrastructure/LAN-DNS.md). # DNS for these names is served by the dnsmasq quadlet on this host -# (dankclaw/dnsmasq/addn-hosts) and handed to LAN clients by the SURFboard -# G36 router (DNS Relay -> statically configured -> 192.168.0.143). +# (dankclaw/dnsmasq/addn-hosts — every name resolves to 192.168.0.143) +# and handed to LAN clients by the SURFboard G36 router. +# +# The basicauth hash below is a placeholder. deploy.sh renders the real +# hash (CADDY_BASICAUTH_HASH from the host .env, git-ignored) into +# /home/dank/Containers/caddy/Caddyfile, which the container mounts. # # Phase 1 uses explicit http:// site addresses (no TLS yet). Phase 2 drops # the prefix and adds a wildcard Let's Encrypt cert via DNS-01 (Porkbun). @@ -30,12 +37,21 @@ http://dankclaw.franksdank.cloud { file_server } -# --- SearXNG (behind the Gluetun tunnel) --- +# --- Legacy name: the media host has no Caddy of its own anymore --- +http://dankserver.franksdank.cloud { + redir http://dankclaw.franksdank.cloud/ 302 +} + +# ═══════════════════════════════════════════════════════════════ +# Services on this host (dankclaw, 192.168.0.143) +# ═══════════════════════════════════════════════════════════════ + +# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) --- http://search.franksdank.cloud { basicauth { dank $2a$14$REPLACE_WITH_HASH } - reverse_proxy gluetun:8080 + reverse_proxy 192.168.0.143:8081 } # --- ComfyUI --- @@ -43,9 +59,7 @@ http://comfyui.franksdank.cloud { reverse_proxy comfyui:8188 } -# --- OpenWebUI --- -# open-webui is a quadlet container on slirp4netns (isolated), so Caddy cannot -# reach it by container name. Proxy to its published LAN port instead. +# --- OpenWebUI (quadlet on slirp4netns; published 3005) --- http://chat.franksdank.cloud { basicauth { dank $2a$14$REPLACE_WITH_HASH @@ -61,9 +75,9 @@ http://hermes.franksdank.cloud { reverse_proxy hermes:8090 } -# --- Gitea (migrated) --- +# --- Gitea (quadlet; published 3000) --- http://gitea.franksdank.cloud { - reverse_proxy gitea-server:3000 + reverse_proxy 192.168.0.143:3000 } # --- Grafana --- @@ -84,8 +98,83 @@ http://prometheus.franksdank.cloud { # --- Loki --- http://loki.franksdank.cloud { + reverse_proxy loki:3100 +} + +# ═══════════════════════════════════════════════════════════════ +# Services on dankserver (192.168.0.235) — published LAN ports +# ═══════════════════════════════════════════════════════════════ + +# --- Plex (existing, kept) --- +http://plex.franksdank.casa { + reverse_proxy 192.168.0.235:32400 +} + +http://plex.franksdank.cloud { + reverse_proxy 192.168.0.235:32400 +} + +# --- Calibre-Web --- +http://books.franksdank.cloud { + reverse_proxy 192.168.0.235:8083 +} + +# --- RomM --- +http://romm.franksdank.cloud { basicauth { dank $2a$14$REPLACE_WITH_HASH } - reverse_proxy loki:3100 + reverse_proxy 192.168.0.235:8085 } + +# --- Copyparty --- +http://files.franksdank.cloud { + basicauth { + dank $2a$14$REPLACE_WITH_HASH + } + reverse_proxy 192.168.0.235:3923 +} + +# --- Jellyfin --- +http://jellyfin.franksdank.cloud { + reverse_proxy 192.168.0.235:8096 + + transport http { + versions h2c 1.1 + } +} + +# --- *arr Stack --- +http://radarr.franksdank.cloud { + reverse_proxy 192.168.0.235:7878 +} + +http://sonarr.franksdank.cloud { + reverse_proxy 192.168.0.235:8989 +} + +http://lidarr.franksdank.cloud { + reverse_proxy 192.168.0.235:8686 +} + +http://readarr.franksdank.cloud { + reverse_proxy 192.168.0.235:8787 +} + +http://prowlarr.franksdank.cloud { + reverse_proxy 192.168.0.235:9696 +} + +http://qbittorrent.franksdank.cloud { + reverse_proxy 192.168.0.235:8080 +} + +# --- Optional: Jackett --- +# http://jackett.franksdank.cloud { +# reverse_proxy 192.168.0.235:9117 +# } + +# --- Optional: Whisparr --- +# http://whisparr.franksdank.cloud { +# reverse_proxy 192.168.0.235:6969 +# } diff --git a/dankclaw/compose.yaml b/dankclaw/compose.yaml index 24f6985..46e5961 100644 --- a/dankclaw/compose.yaml +++ b/dankclaw/compose.yaml @@ -44,23 +44,10 @@ services: retries: 5 start_period: 180s - # --- Chat UI + agent ----------------------------------------------------- - openwebui: - image: ghcr.io/open-webui/open-webui:main - container_name: openwebui - restart: unless-stopped - environment: - OPENAI_API_BASE_URL: "${OPENAI_API_BASE_URL}" - OPENAI_API_KEY: "${OPENAI_API_KEY}" - ports: - - "3000:8080" - volumes: - - /opt/openwebui/data:/app/backend/data - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 + # --- Chat UI (open-webui) ------------------------------------------------ + # Now a boot-persistent Quadlet unit (quadlet, slirp4netns, port 3005), not + # a compose service. See open-webui/quadlet/open-webui.container and + # ../docs/Dankclaw/OpenWebUI.md. hermes: image: docker.io/nousresearch/hermes-agent:latest @@ -107,50 +94,9 @@ services: volumes: - /home/dank/hermes-data:/data - # --- Git hosting (migrated from /opt/gitea) ------------------------------ - postgres: - image: docker.io/library/postgres:16-alpine - container_name: gitea-postgres - restart: unless-stopped - environment: - POSTGRES_USER: gitea - POSTGRES_PASSWORD: "${GITEA_DB_PASSWORD}" - POSTGRES_DB: gitea - volumes: - - /opt/gitea/data/gitea-postgres:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U gitea"] - interval: 30s - timeout: 10s - retries: 3 - - gitea: - image: docker.io/gitea/gitea:latest - container_name: gitea-server - restart: unless-stopped - environment: - DATABASE_TYPE: postgres - DATABASE_HOST: gitea-postgres - DATABASE_NAME: gitea - DATABASE_USER: gitea - DATABASE_PASSWD: "${GITEA_DB_PASSWORD}" - DATABASE_PORT: "5432" - USER_UID: "1000" - USER_GID: "1000" - ports: - - "3001:3000" - - "2222:22" - volumes: - - /opt/gitea/data/gitea-gitea:/data - - /etc/timezone:/etc/timezone:ro - - /etc/localtime:/etc/localtime:ro - depends_on: - - postgres - healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"] - interval: 30s - timeout: 10s - retries: 3 + # --- Git hosting (Gitea) ------------------------------------------------- + # Now boot-persistent Quadlet units (private bridge network, DNS enabled), + # not compose services. See gitea/quadlet/ and ../docs/Dankclaw/Gitea.md. # --- Observability ------------------------------------------------------- prometheus: @@ -247,7 +193,11 @@ services: timeout: 10s retries: 3 - # --- Reverse proxy + homepage ------------------------------------------- + # --- Reverse proxy + homepage (the homelab's single Caddy) --------------- + # The Caddyfile mounted here is the RENDERED copy (basicauth hash filled in + # from the host .env by scripts/deploy.sh); the repo copy carries the + # REPLACE_WITH_HASH placeholder. Certs/config live under + # /home/dank/Containers/caddy (rootless-writable). caddy: image: docker.io/library/caddy:latest container_name: caddy @@ -256,21 +206,20 @@ services: - "80:80" - "443:443" volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - /opt/caddy/data:/data - - /opt/caddy/config:/config + - /home/dank/Containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - /home/dank/Containers/caddy/data:/data + - /home/dank/Containers/caddy/config:/config - ../shared/homepage:/srv/homepage:ro depends_on: - - searxng - comfyui - - openwebui - hermes - - gitea - grafana - prometheus - loki healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"] + # The bare-IP catch-all answers 302 (curl -f passes on <400); this + # proves Caddy itself is serving without touching any upstream. + test: ["CMD", "curl", "-sf", "-o", "/dev/null", "http://localhost:80/"] interval: 30s timeout: 10s retries: 3 diff --git a/dankclaw/dnsmasq/addn-hosts b/dankclaw/dnsmasq/addn-hosts index cec6d0c..a6832b7 100644 --- a/dankclaw/dnsmasq/addn-hosts +++ b/dankclaw/dnsmasq/addn-hosts @@ -1,29 +1,33 @@ # A records for the flat franksdank.name scheme (dnsmasq addn-hosts format: -# [ ...]). Every Caddy vhost name has exactly one record, -# pointing at the host that runs it. Keep in sync with the Caddyfiles -# (tests/dnsmasq.bats cross-checks this). +# [ ...]). # -# 192.168.0.235 = dankserver (media/library stack) -# 192.168.0.143 = dankclaw (AI/search/dev stack) +# Caddy is consolidated on dankclaw (192.168.0.143): it fronts every service +# on both hosts, so every name resolves to that one IP. Keep in sync with the +# Caddyfile (tests/dnsmasq.bats cross-checks this). +# +# 192.168.0.143 = dankclaw (runs the Caddy container + the AI/dev stack) +# 192.168.0.235 = dankserver (media stack; reached by Caddy via published +# LAN ports — no DNS records point here) -# --- dankserver services --- -192.168.0.235 dankserver.franksdank.cloud -192.168.0.235 plex.franksdank.cloud -192.168.0.235 plex.franksdank.casa -192.168.0.235 books.franksdank.cloud -192.168.0.235 llms.franksdank.cloud -192.168.0.235 romm.franksdank.cloud -192.168.0.235 files.franksdank.cloud -192.168.0.235 jellyfin.franksdank.cloud -192.168.0.235 radarr.franksdank.cloud -192.168.0.235 sonarr.franksdank.cloud -192.168.0.235 lidarr.franksdank.cloud -192.168.0.235 readarr.franksdank.cloud -192.168.0.235 prowlarr.franksdank.cloud -192.168.0.235 qbittorrent.franksdank.cloud - -# --- dankclaw services --- +# --- Homepage + legacy name --- 192.168.0.143 dankclaw.franksdank.cloud +192.168.0.143 dankserver.franksdank.cloud + +# --- Media services (dankserver, proxied) --- +192.168.0.143 plex.franksdank.cloud +192.168.0.143 plex.franksdank.casa +192.168.0.143 books.franksdank.cloud +192.168.0.143 romm.franksdank.cloud +192.168.0.143 files.franksdank.cloud +192.168.0.143 jellyfin.franksdank.cloud +192.168.0.143 radarr.franksdank.cloud +192.168.0.143 sonarr.franksdank.cloud +192.168.0.143 lidarr.franksdank.cloud +192.168.0.143 readarr.franksdank.cloud +192.168.0.143 prowlarr.franksdank.cloud +192.168.0.143 qbittorrent.franksdank.cloud + +# --- AI / search / dev services (dankclaw, local) --- 192.168.0.143 search.franksdank.cloud 192.168.0.143 comfyui.franksdank.cloud 192.168.0.143 chat.franksdank.cloud diff --git a/dankclaw/gitea/Caddyfile b/dankclaw/gitea/Caddyfile deleted file mode 100644 index 5a0696d..0000000 --- a/dankclaw/gitea/Caddyfile +++ /dev/null @@ -1,7 +0,0 @@ -# Gitea stack Caddyfile (dedicated Caddy container, quadlet-managed). -# Lives on the host at /opt/gitea/data/caddy/Caddyfile. -# Upstream is the quadlet container name gitea-server (port 3000 inside). - -gitea.franksdank.cloud { - reverse_proxy gitea-server:3000 -} diff --git a/dankserver/.env.example b/dankserver/.env.example index 30d0c98..6a6f4d1 100644 --- a/dankserver/.env.example +++ b/dankserver/.env.example @@ -22,8 +22,3 @@ JELLYFIN_PUBLISH_SERVER_URL=http://192.168.0.235:8096 # *arr Stack ARR_TZ=America/New_York - -# Porkbun API (franksdank.cloud zone). Caddy uses these for DNS-01 -# Let's Encrypt wildcard cert issuance. Scope the token to this zone only. -PORKBUN_API_KEY=changeme -PORKBUN_API_SECRET=changeme diff --git a/dankserver/Caddyfile b/dankserver/Caddyfile deleted file mode 100644 index 3b032a9..0000000 --- a/dankserver/Caddyfile +++ /dev/null @@ -1,109 +0,0 @@ -# Dankserver Caddyfile -# Deploy: copy to /etc/caddy/Caddyfile on dankserver (host Caddy service) -# See ../docs/Dankserver/ for service documentation. -# -# Naming: flat franksdank.cloud scheme (see docs/Infrastructure/Domain-DNS.md). -# DNS for these names is served by the dnsmasq quadlet on dankclaw -# (dankclaw/dnsmasq/addn-hosts) and handed to LAN clients by the SURFboard -# G36 router (DNS Relay -> statically configured -> 192.168.0.143). -# -# Phase 1 uses explicit http:// site addresses (no TLS yet). Phase 2 drops -# the prefix and adds a wildcard Let's Encrypt cert via DNS-01 (Porkbun). - -{ - admin off - email admin@franksdank.cloud -} - -# --- Bare-IP access (health checks, convenience) --- -:80 { - redir http://dankserver.franksdank.cloud/ 302 -} - -# --- Homepage (password protected) --- -http://dankserver.franksdank.cloud { - basicauth { - dank $2a$14$REPLACE_WITH_HASH - } - root * /srv/homepage - file_server -} - -# --- Plex (existing, kept) --- -http://plex.franksdank.casa { - reverse_proxy localhost:32400 -} - -http://plex.franksdank.cloud { - reverse_proxy localhost:32400 -} - -# --- Calibre-Web (existing, kept) --- -http://books.franksdank.cloud { - reverse_proxy localhost:8083 -} - -# --- OpenWebUI (live name from the host Caddyfile; LLM chat UI on this host) --- -http://llms.franksdank.cloud { - reverse_proxy localhost:3000 -} - -# --- RomM --- -http://romm.franksdank.cloud { - basicauth { - dank $2a$14$REPLACE_WITH_HASH - } - reverse_proxy localhost:8085 -} - -# --- Copyparty --- -http://files.franksdank.cloud { - basicauth { - dank $2a$14$REPLACE_WITH_HASH - } - reverse_proxy localhost:3923 -} - -# --- Jellyfin --- -http://jellyfin.franksdank.cloud { - reverse_proxy 127.0.0.1:8096 - - transport http { - versions h2c 1.1 - } -} - -# --- *arr Stack --- -http://radarr.franksdank.cloud { - reverse_proxy localhost:7878 -} - -http://sonarr.franksdank.cloud { - reverse_proxy localhost:8989 -} - -http://lidarr.franksdank.cloud { - reverse_proxy localhost:8686 -} - -http://readarr.franksdank.cloud { - reverse_proxy localhost:8787 -} - -http://prowlarr.franksdank.cloud { - reverse_proxy localhost:9696 -} - -http://qbittorrent.franksdank.cloud { - reverse_proxy localhost:8080 -} - -# --- Optional: Jackett --- -# http://jackett.franksdank.cloud { -# reverse_proxy localhost:9117 -# } - -# --- Optional: Whisparr --- -# http://whisparr.franksdank.cloud { -# reverse_proxy localhost:6969 -# } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 0b2d500..b8d64fc 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -26,17 +26,24 @@ set -euo pipefail # --------------------------------------------------------------------------- declare -A REMOTE_USER REMOTE_HOST REMOTE_DIR COMPOSE HEALTH_URL +# Caddy is consolidated on dankclaw: it fronts both hosts' services. The +# dankserver media stack runs as bare containers (not this repo's compose), +# so its deploy is pull-only; health is checked against the shared Caddy. REMOTE_USER[dankclaw]=dank REMOTE_HOST[dankclaw]=dankclaw REMOTE_DIR[dankclaw]="/home/dank/serverAppConfigs" COMPOSE[dankclaw]="dankclaw/compose.yaml" HEALTH_URL[dankclaw]="http://127.0.0.1/" +COMPOSE_UP[dankclaw]=1 +COPY_SHARED[dankclaw]=1 REMOTE_USER[dankserver]=dank REMOTE_HOST[dankserver]=dankserver REMOTE_DIR[dankserver]="/home/dank/serverAppConfigs" COMPOSE[dankserver]="dankserver/compose.yaml" -HEALTH_URL[dankserver]="http://127.0.0.1/" +HEALTH_URL[dankserver]="http://192.168.0.143/" +COMPOSE_UP[dankserver]=0 +COPY_SHARED[dankserver]=0 TARGET="${1:-}" DRY_RUN=0 @@ -101,21 +108,38 @@ cd '${REMOTE_DIR[$TARGET]}' echo '[2/4] Pulling repo on ${TARGET}...' git pull origin main -echo '[3/4] Copying shared configs into place...' -mkdir -p /opt/searxng/config /opt/prometheus/config /opt/loki/config /opt/alloy/config -cp shared/searxng/settings.yml /opt/searxng/config/settings.yml -cp shared/searxng/limiter.toml /opt/searxng/config/limiter.toml -cp shared/prometheus/prometheus.yml /opt/prometheus/config/prometheus.yml -cp shared/loki/loki-config.yaml /opt/loki/config/loki-config.yaml -cp shared/alloy/config.alloy /opt/alloy/config/config.alloy if [ -f .env ]; then echo ' .env present, using it' else echo ' WARN: no .env on target; copy the .env.example and fill it in' fi -echo '[4/4] Restarting stack...' -podman-compose -f '${COMPOSE[$TARGET]}' up -d --pull=missing +if [ '${COPY_SHARED[$TARGET]}' = '1' ]; then + echo '[3/4] Copying shared configs into place...' + mkdir -p /opt/searxng/config /opt/prometheus/config /opt/loki/config /opt/alloy/config + cp shared/searxng/settings.yml /opt/searxng/config/settings.yml + cp shared/searxng/limiter.toml /opt/searxng/config/limiter.toml + cp shared/prometheus/prometheus.yml /opt/prometheus/config/prometheus.yml + cp shared/loki/loki-config.yaml /opt/loki/config/loki-config.yaml + cp shared/alloy/config.alloy /opt/alloy/config/config.alloy + + echo ' Rendering Caddyfile (basicauth hash from .env)...' + set -a; . ./.env; set +a + if [ -z \"\${CADDY_BASICAUTH_HASH:-}\" ]; then + echo ' WARN: CADDY_BASICAUTH_HASH not set in .env; keeping placeholder' + else + mkdir -p /home/dank/Containers/caddy + sed \"s/REPLACE_WITH_HASH/\${CADDY_BASICAUTH_HASH}/g\" dankclaw/Caddyfile \\ + > /home/dank/Containers/caddy/Caddyfile + fi +fi + +if [ '${COMPOSE_UP[$TARGET]}' = '1' ]; then + echo '[4/4] Restarting stack...' + podman-compose -f '${COMPOSE[$TARGET]}' up -d --pull=missing +else + echo '[4/4] Skipping compose restart (stack runs as bare containers on this host)' +fi " if [ "$DRY_RUN" = "1" ]; then diff --git a/tests/caddyfile.bats b/tests/caddyfile.bats index 714d516..6c48828 100644 --- a/tests/caddyfile.bats +++ b/tests/caddyfile.bats @@ -1,46 +1,60 @@ #!/usr/bin/env bats # Seam: Caddyfile validity. -# Each host must have a Caddyfile. Site blocks must reference services by the -# hostname convention (*.dankserver.local / *.dankclaw.local) and must proxy to -# reachable upstreams. When caddy(1) exists locally we run `caddy validate`; -# otherwise we do structural checks. +# +# There is exactly one Caddy in the homelab: the container on dankclaw +# (dankclaw/compose.yaml, service 'caddy'). It fronts every service on both +# hosts — local services by container name, dankserver services by published +# LAN port. Site blocks use the flat franksdank.cloud / franksdank.casa +# convention. The basicauth hash is a placeholder in git; deploy.sh renders +# the real hash from the host .env (CADDY_BASICAUTH_HASH). load helpers -@test "a Caddyfile exists for dankserver and dankclaw" { - [ -f "$REPO_ROOT/dankserver/Caddyfile" ] - [ -f "$REPO_ROOT/dankclaw/Caddyfile" ] +CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile" + +@test "the Caddyfile lives on dankclaw only (Caddy is consolidated there)" { + [ -f "$CADDYFILE" ] + [ ! -f "$REPO_ROOT/dankserver/Caddyfile" ] } -@test "Caddyfiles use the flat franksdank.cloud hostname convention" { - grep -qE '\.franksdank\.cloud' "$REPO_ROOT/dankserver/Caddyfile" - grep -qE '\.franksdank\.cloud' "$REPO_ROOT/dankclaw/Caddyfile" +@test "the Caddyfile uses the flat franksdank.cloud hostname convention" { + grep -qE '\.franksdank\.cloud' "$CADDYFILE" } -@test "no .local vhost names remain in either Caddyfile" { +@test "no .local vhost names remain in the Caddyfile" { ! grep -qE '^([a-z0-9.-]+\.)?(dankserver|dankclaw)\.local[[:space:]]*\{' \ - "$REPO_ROOT/dankserver/Caddyfile" "$REPO_ROOT/dankclaw/Caddyfile" + "$CADDYFILE" } -@test "Caddyfiles have balanced braces" { - for f in "$REPO_ROOT/dankserver/Caddyfile" "$REPO_ROOT/dankclaw/Caddyfile"; do - local open close - open=$(grep -o '{' "$f" | wc -l) - close=$(grep -o '}' "$f" | wc -l) - [ "$open" -eq "$close" ] || { echo "$f: unbalanced braces ($open vs $close)"; return 1; } - done +@test "the Caddyfile has balanced braces" { + local open close + open=$(grep -o '{' "$CADDYFILE" | wc -l) + close=$(grep -o '}' "$CADDYFILE" | wc -l) + [ "$open" -eq "$close" ] || { echo "unbalanced braces ($open vs $close)"; return 1; } } @test "caddy validate passes when caddy is available" { if command -v caddy >/dev/null 2>&1; then - caddy validate --config "$REPO_ROOT/dankserver/Caddyfile" --adapter caddyfile - caddy validate --config "$REPO_ROOT/dankclaw/Caddyfile" --adapter caddyfile + caddy validate --config "$CADDYFILE" --adapter caddyfile else skip "caddy binary not installed locally" fi } -@test "homepage basic auth exists in the Caddyfiles" { - grep -qiE 'basicauth|basic_auth' "$REPO_ROOT/dankserver/Caddyfile" - grep -qiE 'basicauth|basic_auth' "$REPO_ROOT/dankclaw/Caddyfile" +@test "homepage basic auth exists in the Caddyfile" { + grep -qiE 'basicauth|basic_auth' "$CADDYFILE" +} + +@test "the Caddyfile carries the renderable basicauth placeholder" { + grep -q 'REPLACE_WITH_HASH' "$CADDYFILE" +} + +@test "compose no longer defines the quadlet-replaced services" { + # gitea, its postgres, and open-webui run as boot-persistent quadlets on + # dankclaw (see dankclaw/gitea/quadlet, dankclaw/open-webui/quadlet). + ! grep -qE '^\s+(gitea|postgres|openwebui):\s*$' "$REPO_ROOT/dankclaw/compose.yaml" +} + +@test "the caddy service healthcheck probes the bare-IP catch-all" { + grep -q 'curl.*-sf.*http://localhost:80/' "$REPO_ROOT/dankclaw/compose.yaml" } diff --git a/tests/dnsmasq.bats b/tests/dnsmasq.bats index 1eaf80d..b700dab 100644 --- a/tests/dnsmasq.bats +++ b/tests/dnsmasq.bats @@ -84,18 +84,10 @@ record_ip_for() { awk -v n="$1" '{ip=$1; for (i=2; i<=NF; i++) if ($i==n) print ip}' "$HOSTS" | head -1 } -@test "every dankserver Caddyfile vhost has an A record on 192.168.0.235" { - local names n ip - names="$(vhost_names "$REPO_ROOT/dankserver/Caddyfile")" - [ -n "$names" ] || { echo "no .franksdank vhosts found in dankserver/Caddyfile"; return 1; } - while IFS= read -r n; do - [ -z "$n" ] && continue - ip="$(record_ip_for "$n")" - [ "$ip" = "192.168.0.235" ] || { echo "$n -> '$ip' (want 192.168.0.235)"; return 1; } - done <<< "$names" -} +# Caddy is consolidated on dankclaw (192.168.0.143): it fronts every service +# on both hosts, so every vhost name resolves to that one IP. -@test "every dankclaw Caddyfile vhost has an A record on 192.168.0.143" { +@test "every Caddyfile vhost has an A record on 192.168.0.143" { local names n ip names="$(vhost_names "$REPO_ROOT/dankclaw/Caddyfile")" [ -n "$names" ] || { echo "no .franksdank vhosts found in dankclaw/Caddyfile"; return 1; } @@ -106,6 +98,14 @@ record_ip_for() { done <<< "$names" } +@test "no addn-hosts records point at the old per-host split (192.168.0.235)" { + ! awk '!/^[[:space:]]*#/ && NF {print $1}' "$HOSTS" | grep -q '^192\.168\.0\.235$' +} + +@test "the retired llms name has no A record" { + ! grep -qE '\billms\.franksdank\.cloud\b' "$HOSTS" +} + @test "no secret material in the dnsmasq files" { local f for f in "$DNSMASQ_DIR"/*; do