- 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.
139 lines
6.0 KiB
Bash
Executable File
139 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# Seam: the service inventory (rank 3). shared/services.yml is the one
|
|
# inventory of deployable services; scripts/service-map.py is its only
|
|
# reader, and every consumer (deploy-plan.py, deploy-remote.sh's
|
|
# allow-list, deploy.sh's --service gate, test-changed.sh's seam mapper,
|
|
# the Pi-hole hosts render, the Prometheus scrape facts, the homepage
|
|
# probes) goes through it. These tests pin the inventory's integrity,
|
|
# the planner's selection against a captured fixture, and the rendered
|
|
# artifacts' drift against the tracked copies.
|
|
|
|
load helpers
|
|
|
|
@test "inventory: schema, unit claims, path claims, compose ownership, and render drift are sound" {
|
|
run python3 "$REPO_ROOT/scripts/service-map.py" check
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "inventory: the deployers' allow-list is the inventory" {
|
|
# Every service the inventory places on a host must pass "allows" for
|
|
# that host, and a name the inventory never places must be refused.
|
|
for host in dankclaw dankserver frankbook-pro; do
|
|
while IFS= read -r svc; do
|
|
[ -n "$svc" ] || continue
|
|
run python3 "$REPO_ROOT/scripts/service-map.py" allows --host "$host" --service "$svc"
|
|
[ "$status" -eq 0 ] || { echo "allows refused $host:$svc"; return 1; }
|
|
done < <(python3 "$REPO_ROOT/scripts/service-map.py" services --host "$host")
|
|
done
|
|
run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankserver --service sonarr
|
|
[ "$status" -ne 0 ] # the *arr stack lives on frankbook-pro, not dankserver
|
|
run python3 "$REPO_ROOT/scripts/service-map.py" allows --host dankclaw --service frank-archon
|
|
[ "$status" -ne 0 ] # frank-archon lives on dankserver
|
|
}
|
|
|
|
@test "inventory: the planner reproduces the pinned selection fixture" {
|
|
# tests/fixtures/service-plan-golden.tsv is the characterization of
|
|
# path -> service selection across every tracked path, every target,
|
|
# and the multi-path combos the old hand-written planner was fed.
|
|
python3 - "$REPO_ROOT" <<'PYEOF'
|
|
import importlib.util
|
|
import sys
|
|
|
|
root = sys.argv[1]
|
|
spec = importlib.util.spec_from_file_location("sm", root + "/scripts/service-map.py")
|
|
sm = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(sm)
|
|
inv = sm.Inventory.load(root + "/shared/services.yml")
|
|
bad = []
|
|
with open(root + "/tests/fixtures/service-plan-golden.tsv") as fh:
|
|
for line in fh:
|
|
host, paths, want = line.rstrip("\n").split("\t")
|
|
got = ",".join(sm.select(inv, host, paths.split("+")))
|
|
if got != want:
|
|
bad.append(f"{host} {paths}\n want: {want}\n got: {got}")
|
|
if bad:
|
|
print(f"{len(bad)} selection mismatches:", *bad[:20], sep="\n")
|
|
sys.exit(1)
|
|
PYEOF
|
|
}
|
|
|
|
@test "inventory: deploy-plan.py keeps its CLI contract on top of service-map" {
|
|
# stdin paths, positional paths, and the --repo/--base/--head diff mode
|
|
# all route to the same selection.
|
|
run bash -c "cd '$REPO_ROOT' && printf '%s\n' dankclaw/searxng/quadlet/gluetun.container | \
|
|
scripts/deploy-plan.py --target dankclaw"
|
|
[ "$status" -eq 0 ]
|
|
[ "${lines[0]}" = "gluetun" ]
|
|
run "$REPO_ROOT/scripts/deploy-plan.py" --target dankserver scripts/frank-archon.env
|
|
[ "$status" -eq 0 ]
|
|
[ "${lines[0]}" = "frank-archon" ]
|
|
run "$REPO_ROOT/scripts/deploy-plan.py" --target dankclaw --repo "$REPO_ROOT" --base HEAD --head HEAD
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "inventory: the Pi-hole hosts file is a rendered artifact" {
|
|
# The tracked addn-hosts must be byte-identical to the render; editing
|
|
# it by hand is drift the inventory check also catches.
|
|
run bash -c "cd '$REPO_ROOT' && python3 scripts/service-map.py render pihole-hosts | \
|
|
diff -u dankclaw/pihole/addn-hosts -"
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
|
|
@test "inventory: changed-test seams for a service unit are the service's own" {
|
|
# The rank-3 contract: a unit change selects the OWNING service's
|
|
# declared seams, not the old shotgun of unrelated quadlet seams.
|
|
run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths dankclaw/gitea/quadlet/gitea.network
|
|
[ "$status" -eq 0 ]
|
|
[[ "$output" == *"tests/caddyfile.bats"* ]]
|
|
[[ "$output" == *"tests/deployment_scoping.bats"* ]]
|
|
[[ "$output" != *"tests/open_webui_quadlet.bats"* ]]
|
|
[[ "$output" != *"tests/searxng_quadlet.bats"* ]]
|
|
}
|
|
|
|
@test "inventory: every tracked quadlet unit is claimed by exactly one service" {
|
|
# check units covers claim conflicts; this pins the census size so a
|
|
# unit added WITHOUT an inventory entry (unclaimed) fails the seam.
|
|
python3 - "$REPO_ROOT" <<'PYEOF'
|
|
import importlib.util
|
|
import subprocess
|
|
import sys
|
|
from pathlib import PurePosixPath
|
|
|
|
root = sys.argv[1]
|
|
spec = importlib.util.spec_from_file_location("sm", root + "/scripts/service-map.py")
|
|
sm = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(sm)
|
|
inv = sm.Inventory.load(root + "/shared/services.yml")
|
|
graph = sm.graph_for(tuple(inv.hosts))
|
|
owned = {u for u in graph.units}
|
|
problems = []
|
|
tracked_units = [
|
|
p for p in sm.tracked_files()
|
|
if PurePosixPath(p).name.endswith((".container", ".network")) and not sm.is_trash(p)
|
|
]
|
|
for path in tracked_units:
|
|
parts = PurePosixPath(path).parts
|
|
name = parts[-1]
|
|
key = (parts[0], name.rsplit(".", 1)[0])
|
|
if key not in graph.units and key not in graph.networks:
|
|
problems.append(f"unclaimed unit-file path: {path}")
|
|
if problems:
|
|
print("\n".join(problems))
|
|
sys.exit(1)
|
|
PYEOF
|
|
}
|
|
|
|
@test "inventory: the node-exporter unit is a per-host rendered template" {
|
|
# Rank 6: the two node-exporter quadlets were a MIRROR pair kept in
|
|
# step by a comment. The render is now the law on both hosts.
|
|
for host in dankclaw dankserver; do
|
|
run bash -c "cd '$REPO_ROOT' && python3 scripts/service-map.py render node-exporter-unit --host $host | \
|
|
diff -u $host/observability/quadlet/node-exporter.container -"
|
|
[ "$status" -eq 0 ] || { echo "$host unit drift"; return 1; }
|
|
done
|
|
# Hand-edited copies fail the drift gate:
|
|
grep -q "Keep this file in step" dankserver/observability/quadlet/node-exporter.container && return 1
|
|
true
|
|
}
|