#!/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, which is the only file enable symlinks into ~/Server # * exactly one podman-*.service unit -- its filename is the unit's # identity, so two would be an ambiguity and zero an unenableable service # * a WorkingDirectory of %h/Server/, 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)" containers_dir="$repo_dir/server/containers" 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"/*.service) if (( ${#units[@]} == 0 )); then note "$name has no unit file" continue elif (( ${#units[@]} > 1 )); then note "$name has ${#units[@]} unit files; its identity is ambiguous" continue fi unit="${units[0]}" case "$(basename "$unit")" in podman-*.service) ;; *) note "$name's unit is not named podman-.service: $(basename "$unit")" ;; esac 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'