#!/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