185 lines
8.5 KiB
Bash
185 lines
8.5 KiB
Bash
#!/usr/bin/env bash
|
|
# What berth has settled, and what it has withdrawn, written down as assertions.
|
|
#
|
|
# Two halves:
|
|
# - decisions that hold. Failing one means "you are about to undo this".
|
|
# - every entry in ../STALE.md. Failing one means a withdrawn assumption came
|
|
# back. That is the half that makes STALE.md an audit surface and not an
|
|
# archive — a retraction nobody re-reads is a retraction that decays.
|
|
#
|
|
# Scope: no cloud, no ssh, no sudo, no network. Cheap enough to actually run.
|
|
# `make check` reports on the world and never fails; this exits 1, like rig's.
|
|
#
|
|
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
|
set -uo pipefail # NOT -e: one failing check must not abort the rest
|
|
cd "$(dirname "$0")"
|
|
|
|
source ./lib/config.sh
|
|
|
|
rc=0
|
|
passed=0
|
|
check() { # name, expected, actual
|
|
if [ "$2" = "$3" ]; then
|
|
printf ' ok %s\n' "$1"
|
|
passed=$((passed + 1))
|
|
else
|
|
printf ' FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$3"
|
|
rc=1
|
|
fi
|
|
}
|
|
note() { printf '\n%s\n' "$1"; }
|
|
skip() { printf ' skip %s (%s)\n' "$1" "$2"; }
|
|
|
|
# An absence check must not match the files that RECORD the absence. STALE.md
|
|
# names every withdrawn thing by definition, and this file names them again to
|
|
# assert them — so both are excluded, or every check fails on itself. rig hits
|
|
# the same wall and assembles its pattern from fragments for the same reason.
|
|
NOSELF="--exclude=selftest.sh --exclude=STALE.md"
|
|
absent() { grep -rIl $NOSELF "$@" 2>/dev/null | wc -l; }
|
|
|
|
# A throwaway estate, for the checks that have to run berth rather than read it.
|
|
TMP_ESTATE=_selftest
|
|
cleanup() { rm -f "../estate/${TMP_ESTATE}.json"; }
|
|
trap cleanup EXIT
|
|
|
|
note "the withdrawn assumptions — ../STALE.md, one check each"
|
|
|
|
# B1 — Pulumi. The two surviving mentions are historical fact about ppl/infra
|
|
# and live in README.md and the estate, not in anything that runs.
|
|
check "B1 no pulumi in the code" "0" "$(absent -i pulumi . ../Makefile)"
|
|
|
|
# B2 — the ctlptl precedent. Withdrawn; the argument stands on its own now.
|
|
check "B2 the withdrawn precedent is cited nowhere" "0" "$(absent -i ctlptl ..)"
|
|
|
|
# B3 — `wg show <if> dump` leaks the private key in field 1. Stating only
|
|
# show-vs-showconf makes the dump form read as safe.
|
|
check "B3 all three wg forms are named" "yes" \
|
|
"$(grep -q 'dump' vpn.sh && grep -q 'showconf' vpn.sh && echo yes || echo no)"
|
|
check "B3 capture refuses showconf-shaped input" "1" \
|
|
"$(printf '[Interface]\nPrivateKey = x\n' | bash vpn.sh capture >/dev/null 2>&1; echo $?)"
|
|
check "B3 capture refuses dump-shaped input" "1" \
|
|
"$(printf 'priv\tpub\t51820\toff\n' | bash vpn.sh capture >/dev/null 2>&1; echo $?)"
|
|
|
|
# B4 — keepalive belongs to the peer that DIALS, not the one that roams. The
|
|
# first version warned on a correctly configured overlay, so the check is run
|
|
# against one: hub carries the keepalive, nrft roams.
|
|
python3 - <<'PY'
|
|
import json, collections
|
|
d = json.load(open("../estate/mcrn.json"), object_pairs_hook=collections.OrderedDict)
|
|
d["vpn"]["overlays"]["estate"]["peers"]["box"]["keepalive"] = 25
|
|
json.dump(d, open("../estate/_selftest.json", "w"), indent=2, ensure_ascii=False)
|
|
PY
|
|
check "B4 a correct overlay raises no keepalive warning" "0" \
|
|
"$(ESTATE=$TMP_ESTATE bash vpn.sh check 2>/dev/null | grep -ci 'no peer entry carries')"
|
|
|
|
# B6 — public keys are 44-char base64 too, so shape alone would flag correct
|
|
# data. Same fixture, with a real-shaped public key on a peer.
|
|
python3 - <<'PY'
|
|
import base64, collections, json, os
|
|
d = json.load(open("../estate/_selftest.json"), object_pairs_hook=collections.OrderedDict)
|
|
d["vpn"]["overlays"]["estate"]["peers"]["box"]["public_key"] = base64.b64encode(os.urandom(32)).decode()
|
|
json.dump(d, open("../estate/_selftest.json", "w"), indent=2, ensure_ascii=False)
|
|
PY
|
|
check "B6 a public key does not trip the secret check" "0" \
|
|
"$(ESTATE=$TMP_ESTATE bash vpn.sh check 2>/dev/null | grep -c 'FAIL.*key')"
|
|
|
|
cleanup # the fixture is done with; two estate files would make load_config
|
|
# refuse to guess below, which is right but reads as a config failure
|
|
|
|
# B5 — the pass-through block must be LAST, or a subcommand that names a real
|
|
# target runs that target too. Checked through make, not by reading the file.
|
|
note "B5 a subcommand that names a target dispatches once"
|
|
for combo in "host ports" "host services" "vpn check" "vpn show estate" "estate show"; do
|
|
check " make $combo" "1" \
|
|
"$(cd .. && make -n $combo 2>/dev/null | grep -c 'bash ctrl/')"
|
|
done
|
|
|
|
# B7 — the overlay moved out of network.wireguard into a top-level vpn block.
|
|
check "B7 nothing reads network.wireguard" "0" "$(absent 'network\.wireguard' .)"
|
|
|
|
# B8 — peers, not relatives. berth sources nothing from rig.
|
|
check "B8 berth sources nothing from rig" "0" "$(absent -E 'rig/ctrl|\.\./rig' .)"
|
|
|
|
# B9 — langfuse was filed as an exception a template could not express. It was
|
|
# the general case. The proof is a live route: render it and diff against the
|
|
# hand-written file, normalised for comments and whitespace.
|
|
LIVE=/home/mariano/wdir/semester/ppl/gateway/nginx/conf.d/langfuse.conf
|
|
if [ -f "$LIVE" ]; then
|
|
norm() { sed -e 's/#.*//' -e 's/[[:space:]]\+/ /g' -e 's/^ //' -e 's/ $//' -e '/^$/d' "$1"; }
|
|
bash services.sh render aws >/dev/null 2>&1
|
|
check "B9 the generated vhost reproduces the live one" "same" \
|
|
"$(diff -q <(norm ./render/out/aws/langfuse.conf) <(norm "$LIVE") >/dev/null 2>&1 \
|
|
&& echo same || echo different)"
|
|
else
|
|
skip "B9 generated vhost matches the live one" "ppl not on this machine"
|
|
fi
|
|
|
|
note "the safety contract — berth's verbs are not all safe"
|
|
|
|
check "estate defaults to show" "show" "$(cd .. && make -n estate 2>/dev/null | grep -oE 'estate\.sh [a-z]+' | awk '{print $2}')"
|
|
check "certs defaults to status" "status" "$(cd .. && make -n certs 2>/dev/null | grep -oE 'certs\.sh [a-z]+' | awk '{print $2}')"
|
|
check "dns defaults to list" "list" "$(cd .. && make -n dns 2>/dev/null | grep -oE 'dns\.sh [a-z]+' | awk '{print $2}')"
|
|
check "vpn defaults to list" "list" "$(cd .. && make -n vpn 2>/dev/null | grep -oE 'vpn\.sh [a-z]+' | awk '{print $2}')"
|
|
|
|
for verb in apply destroy; do
|
|
check "estate $verb refuses without --yes" "1" \
|
|
"$(bash estate.sh "$verb" >/dev/null 2>&1; echo $?)"
|
|
done
|
|
for verb in renew push; do
|
|
check "certs $verb refuses" "1" \
|
|
"$(bash certs.sh "$verb" >/dev/null 2>&1; echo $?)"
|
|
done
|
|
check "dns add refuses to change live DNS" "1" \
|
|
"$(bash dns.sh add selftest >/dev/null 2>&1; echo $?)"
|
|
check "vpn up refuses without --yes" "1" \
|
|
"$(bash vpn.sh up >/dev/null 2>&1; echo $?)"
|
|
|
|
note "config — the caller's env beats the files"
|
|
|
|
# Generated from CONFIG_OVERRIDABLE, so a new key enrols itself.
|
|
test_value() {
|
|
case "$1" in
|
|
TARGET) echo "gcp" ;;
|
|
ESTATE) echo "mcrn" ;;
|
|
*) echo "selftest-sentinel" ;;
|
|
esac
|
|
}
|
|
for key in $CONFIG_OVERRIDABLE; do
|
|
want="$(test_value "$key")"
|
|
got="$(export "$key=$want"; load_config >/dev/null 2>&1; echo "${!key}")"
|
|
check " caller's $key wins" "$want" "$got"
|
|
done
|
|
|
|
note "rig agreement — recomputed, never imported"
|
|
|
|
# rig pins these same constants in its own selftest. Both arrive at them from
|
|
# the same formula with no shared code, which is the coupling rule made testable.
|
|
check "derive_port_base rig" "20310" "$(derive_port_base rig)"
|
|
check "derive_port_base foo" "21690" "$(derive_port_base foo)"
|
|
check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
|
|
|
note "containment — berth writes nothing outside berth/"
|
|
|
|
check "no tracked change outside berth/" "0" \
|
|
"$(cd ../.. && git status --porcelain 2>/dev/null | grep -vc '^.. berth/')"
|
|
check "generated output is ignored" "yes" \
|
|
"$(cd .. && git check-ignore -q ctrl/render/out && echo yes || echo no)"
|
|
# A trailing-slash pattern matches directories only, so ask about a path
|
|
# inside it rather than the (not-yet-existing) directory itself.
|
|
check "key material is ignored" "yes" \
|
|
"$(cd .. && git check-ignore -q ctrl/.secrets/vpn/any.key && echo yes || echo no)"
|
|
|
|
note "every STALE entry has a check here"
|
|
|
|
# Not "$0": line 15 cd's into this script's directory, so a relative $0 no
|
|
# longer resolves. After the cd the file is simply selftest.sh.
|
|
# Ids are counted wherever they appear — B5's sits in a note(), not a check name.
|
|
entries="$(grep -c '^\*\*✖ B' ../STALE.md)"
|
|
checked="$(grep -oE '\bB[1-9][0-9]?\b' selftest.sh | sort -u | wc -l)"
|
|
check "STALE.md entries are all covered" "$entries" "$checked"
|
|
|
|
printf '\n%d passed' "$passed"
|
|
[ "$rc" -ne 0 ] && printf ', SOME FAILED'
|
|
printf '\n'
|
|
exit "$rc"
|