Luigit
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