Panama learns what a server is: from a root login to running containers
A machine's role is now the interview's first question and the one answer Panama records. Servers get the same shell minus the screen: core packages, nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless ports from 80, firewalld, the nginx-bridge network, and a nightly image updater that replaced watchtower for cause. server/containers/ carries junior's 23 compose services -- secrets moved to per-machine .env files that never enter this public repo, every transformed compose proven to render byte-identical to what is live. 'panama server' enables, disables and relinks them; nothing here restarts a running service. 'boot --server' walks a fresh VPS from its root login to a normal install. Five new contracts pin the secrets rule, the catalog's shape, panama-server's behavior, the role plumbing, and the dotfile classification. Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
This commit is contained in:
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/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/<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)"
|
||||
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-<name>.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'
|
||||
Reference in New Issue
Block a user