Files
Panama/tests/server/containers-shape-contract
T

83 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Every service in the catalog has the same shape, because panama-server and
# the update timer both depend on it blindly:
#
# * a compose.yml, linked into ~/Server along with any helper script
# * exactly one podman-*.service unit -- its filename is the unit's
# identity, so two would be an ambiguity and zero an unenableable service.
# Other *.service and *.timer files are helpers (a cert refresh, say)
# that enable links beside it; each helper service needs a timer or a
# unit that pulls it in, so an orphan helper is flagged
# * a WorkingDirectory of %h/Server/<Name>, matching the directory enable
# creates -- a unit pointing anywhere else starts compose against a
# directory that has no .env and no data
# * every ${VAR} the compose interpolates without a default is named in
# .env.example, or the first enable renders a compose full of empty
# strings and the service runs misconfigured rather than failing
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# The VPS catalog is what panama-server installs from. server/home/ shares the
# layout but is not installable yet, so it is held to the secrets contract only.
containers_dir="$repo_dir/server/vps"
findings=()
note() { findings+=("$1"); }
[[ -d "$containers_dir" ]] || { printf 'containers shape contract: PASS (no services yet)\n'; exit 0; }
shopt -s nullglob
for dir in "$containers_dir"/*/; do
name="$(basename "$dir")"
[[ -f "$dir/compose.yml" ]] || note "$name has no compose.yml"
units=("$dir"/podman-*.service)
if (( ${#units[@]} == 0 )); then
note "$name has no podman-<name>.service unit"
continue
elif (( ${#units[@]} > 1 )); then
note "$name has ${#units[@]} podman-*.service units; its identity is ambiguous"
continue
fi
unit="${units[0]}"
for helper in "$dir"/*.service; do
[[ "$helper" == "$unit" ]] && continue
[[ -f "${helper%.service}.timer" ]] \
|| note "$name's helper $(basename "$helper") has no matching .timer"
done
grep -q "^WorkingDirectory=%h/Server/$name\$" "$unit" \
|| note "$name's unit does not work in %h/Server/$name"
grep -q 'podman compose' "$unit" \
|| note "$name's unit does not run podman compose"
# ${VAR} without a :- default has nowhere to come from but the .env, and
# the .env is seeded from .env.example -- so a variable the example does
# not name is one the first enable silently renders empty.
[[ -f "$dir/compose.yml" ]] || continue
while IFS= read -r var; do
[[ -n "$var" ]] || continue
if [[ ! -f "$dir/.env.example" ]]; then
note "$name interpolates \${$var} but has no .env.example"
continue
fi
grep -qE "^${var}=" "$dir/.env.example" \
|| note "$name interpolates \${$var}, which .env.example does not name"
done < <(grep -vE '^[[:space:]]*#' "$dir/compose.yml" 2>/dev/null \
| grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}' \
| sed 's/^\${//; s/}$//' | sort -u)
done
if (( ${#findings[@]} > 0 )); then
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
printf 'containers shape contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'containers shape contract: PASS\n'