repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
scripts/local/verify-infra.sh
Raw#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
usage() {
echo "usage: verify-infra.sh [tofu-route|quadlets]" >&2
exit 2
}
scope="${1:-all}"
[[ "$#" -le 1 ]] || usage
[[ "${scope}" == all || "${scope}" == tofu-route || "${scope}" == quadlets ]] || usage
scratch_root="${repo_root}/.verify-infra"
mkdir -p "${scratch_root}"
work="$(mktemp -d "${scratch_root}/verify-infra.XXXXXX")"
trap 'rm -rf -- "${work}"' EXIT
source_dir="${work}/source"
home_dir="${work}/home"
tf_data_dir="${work}/tf-data"
quadlet_generator="${QUADLET_GENERATOR:-/usr/lib/systemd/system-generators/podman-system-generator}"
mkdir -p "${source_dir}" "${home_dir}" "${tf_data_dir}"
copy_file() {
local relative="$1"
local required="$2"
local source="${repo_root}/${relative}"
local destination="${source_dir}/${relative}"
local ancestor="${source}"
if [[ ! -e "${source}" ]]; then
[[ "${required}" == optional ]] && return
echo "required infrastructure source is missing: ${relative}" >&2
exit 1
fi
while [[ "${ancestor}" != "${repo_root}" ]]; do
if [[ -L "${ancestor}" ]]; then
echo "selected infrastructure source has a symlink ancestor: ${relative}" >&2
exit 1
fi
ancestor="$(dirname "${ancestor}")"
done
mkdir -p "$(dirname "${destination}")"
cp -- "${source}" "${destination}"
}
is_selected() {
# shellcheck disable=SC2221,SC2222 # Paths are intentionally tested as a whitelist.
case "$1" in
*.tf | *.tf.json | .terraform.lock.hcl | templates/*.tftpl | modules/klops/*.tf | modules/klops/templates/* | modules/klops/quadlets/* | modules/klops/ci-quadlets/* | services/toad/* | services/luigit/* | sites/index/* | sites/personal/* | sites/genie/* | sites/pkg/* | scripts/local/klops.sh | scripts/local/deploy-toad.sh | scripts/local/build-remote-image.sh | scripts/remote/*.sh | assets/bugabinga.min.svg | assets/nugu.css | assets/jellyfin-branding.xml | assets/registry/index-BAkATJzA.css | assets/dawarich/application-b5a7662b4bd877f1de1fe5769d2b98adfa15a8095b784d392ae1154df6f3b865.js)
return 0
;;
esac
return 1
}
while IFS= read -r -d '' file; do
is_selected "${file}" || continue
case "${file}" in
.env* | */.env* | .terraform/* | */.terraform/* | terraform.tfstate* | */terraform.tfstate* | *.tfvars | *.tfvars.* | *.tfplan | *.tfplan.* | modules/klops/generated/* | modules/klops/toad/*)
continue
;;
esac
copy_file "${file}" optional
done < <(cd "${repo_root}" && git ls-files -z)
while IFS= read -r -d '' file; do
case "${file}" in
.verify-infra/*) ;;
*.tf | *.tf.json | .terraform.lock.hcl)
echo "untracked infrastructure source is not verified: ${file}" >&2
exit 1
;;
esac
done < <(cd "${repo_root}" && git ls-files --others --exclude-standard -z)
for required in backend.tf main.tf outputs.tf providers.tf secrets.tf variables.tf .terraform.lock.hcl modules/klops/main.tf modules/klops/providers.tf modules/klops/toad.tf modules/klops/variables.tf modules/klops/pkg-site.tf modules/klops/templates/Caddyfile.tftpl services/toad/deploy/gateway.Caddyfile; do
copy_file "${required}" required
done
# Validation hashes deployment-only secrets, so provide content-free fixtures instead of copying them.
mkdir -p "${source_dir}/modules/klops/toad"
: >"${source_dir}/modules/klops/toad/enrollment.json"
: >"${source_dir}/modules/klops/toad/ingress.env"
run_tofu() {
/usr/bin/env -i \
PATH="${PATH}" \
LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" \
HOME="${home_dir}" \
TF_DATA_DIR="${tf_data_dir}" \
TF_IN_AUTOMATION=1 \
tofu "$@"
}
run_quadlet() {
local directory="$1"
local output="$2"
local errors="$3"
QUADLET_UNIT_DIRS="${directory}" "${quadlet_generator}" --user --dryrun >"${output}" 2>"${errors}" || {
cat "${errors}" >&2
return 1
}
if grep -Eqi 'error|failed|parse|convert|invalid' "${errors}"; then
cat "${errors}" >&2
return 1
fi
}
check_caddy_format() {
local file="$1"
local formatted
formatted="${work}/$(basename "${file}").formatted"
caddy fmt "${file}" >"${formatted}"
cmp -s "${file}" "${formatted}" && return
diff -u "${file}" "${formatted}" >&2 || true
return 1
}
assert_quadlet_outputs() {
local directory="$1"
local output="$2"
local file name unit
while IFS= read -r -d '' file; do
name="$(basename "${file}")"
case "${name}" in
*.container) unit="${name%.container}.service" ;;
*.pod) unit="${name%.pod}-pod.service" ;;
*.network) unit="${name%.network}-network.service" ;;
*.volume) unit="${name%.volume}-volume.service" ;;
*) continue ;;
esac
grep -Fqx -- "---${unit}---" "${output}" >/dev/null || {
echo "Quadlet generator did not emit ${unit}" >&2
return 1
}
done < <(find "${directory}" -type f -print0)
}
if [[ "${scope}" == all || "${scope}" == tofu-route ]]; then
(
cd "${source_dir}"
run_tofu version
run_tofu fmt -check -recursive
run_tofu init -backend=false -input=false -lockfile=readonly
run_tofu validate
check_caddy_format modules/klops/templates/Caddyfile.tftpl
check_caddy_format services/toad/deploy/gateway.Caddyfile
caddy_dir="${work}/caddy"
mkdir -p "${caddy_dir}"
cp modules/klops/templates/Caddyfile.tftpl "${caddy_dir}/Caddyfile"
printf 'basic_auth {\n admin %s\n}\n' "\$2a\$14\$012345678901234567890uKDeHn0xi2oABCD123456789012345678" >"${caddy_dir}/genie-basicauth.caddy"
cp "${caddy_dir}/genie-basicauth.caddy" "${caddy_dir}/toad-basicauth.caddy"
(cd "${caddy_dir}" && caddy adapt --config Caddyfile --adapter caddyfile) | jq -e '
any(.. | objects; .dial? == "unix//run/toad-ingress/http.sock")
and any(.. | objects | select(has("host")); .host == ["toad.bugabinga.net"])
' >/dev/null
TOAD_WEB_HOST=toad.invalid caddy adapt --config services/toad/deploy/gateway.Caddyfile --adapter caddyfile | jq -e '
all(.apps.http.servers[].listen[]; startswith("unix/"))
and any(.. | objects | select(has("host")); .host == ["toad.invalid"] and .path == ["/", "/assets/*"])
' >/dev/null
)
fi
if [[ "${scope}" == all || "${scope}" == quadlets ]]; then
(
cd "${source_dir}"
rendered="${work}/rendered-quadlets"
mkdir -p "${rendered}/ci" "${rendered}/generated"
cp -a modules/klops/ci-quadlets/. "${rendered}/ci/"
sed "s/\${luci_ssh_peer_port}/2223/g" modules/klops/templates/luci.container.tftpl >"${rendered}/ci/luci.container"
sed -e "s/\${relay_ipv4}/192.0.2.1/g" -e "s/\${relay_ipv6}/2001:db8::1/g" modules/klops/templates/ddns.container.tftpl >"${rendered}/generated/ddns.container"
sed "s/\${vcs_ssh_peer_port}/23231/g" modules/klops/templates/soft-serve.container.tftpl >"${rendered}/generated/soft-serve.container"
cp modules/klops/quadlets/reverse.network "${rendered}/generated/reverse.network"
if grep -RE '\$\{[^}]+\}' "${rendered}"; then
echo "unresolved Quadlet placeholder" >&2
exit 1
fi
run_quadlet "${source_dir}/modules/klops/quadlets" "${work}/quadlets.out" "${work}/quadlets.err"
assert_quadlet_outputs "${source_dir}/modules/klops/quadlets" "${work}/quadlets.out"
run_quadlet "${source_dir}/services/toad/deploy" "${work}/toad.out" "${work}/toad.err"
assert_quadlet_outputs "${source_dir}/services/toad/deploy" "${work}/toad.out"
run_quadlet "${rendered}/ci" "${work}/ci.out" "${work}/ci.err"
assert_quadlet_outputs "${rendered}/ci" "${work}/ci.out"
run_quadlet "${rendered}/generated" "${work}/generated.out" "${work}/generated.err"
assert_quadlet_outputs "${rendered}/generated" "${work}/generated.out"
)
fi