#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HOMELAB_STATE_DIR="${HOMELAB_STATE_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/homelab}" KUBECONFIG="${KUBECONFIG:-${KUBECONFIG_PATH:-${HOME}/.kube/config}}" section() { printf '\n== %s ==\n' "$1" } check_file() { local label="$1" local path="$2" printf '%-34s ' "${label}" if [[ -s "${path}" ]]; then printf 'ok - %s\n' "${path}" else printf 'fail - missing or empty: %s\n' "${path}" return 1 fi } latest_file() { local directory="$1" local glob="$2" find "${directory}" -maxdepth 1 -type f -name "${glob}" -print0 2>/dev/null | xargs -0 ls -t 2>/dev/null | sed -n '1p' } drill_gitea() { local backup_dir="${LAB_GITEA_BACKUP_DIR:-/home/jv/backups/gitea}" local latest section "Gitea Restore Drill" latest="$(latest_file "${backup_dir}" 'gitea-*.zip' || true)" if [[ -z "${latest}" ]]; then printf 'fail - no Gitea backup archive found in %s\n' "${backup_dir}" return 1 fi python3 - "${latest}" <<'PY' import sys import zipfile archive = sys.argv[1] with zipfile.ZipFile(archive) as handle: bad = handle.testzip() if bad: raise SystemExit(f"fail - corrupt archive member: {bad}") members = handle.namelist() if not members: raise SystemExit("fail - archive is empty") print(f"ok - archive valid: {archive} ({len(members)} members)") PY } drill_pihole() { section "Pi-hole Restore Drill" check_file "adlists" "${REPO_ROOT}/infra/rpi-services/adlists.txt" check_file "local DNS records" "${REPO_ROOT}/infra/rpi-services/local-dns-records.txt" check_file "CNAME records" "${REPO_ROOT}/infra/rpi-services/cname-records.txt" check_file "static DHCP notes" "${REPO_ROOT}/infra/rpi-services/static-dhcp-hosts.txt" check_file "RPi compose" "${REPO_ROOT}/infra/rpi-services/docker-compose.yml" check_file "RPi bootstrap" "${REPO_ROOT}/infra/rpi-services/bootstrap.sh" } drill_state() { local backup_dir="${HOMELAB_TOFU_STATE_BACKUP_DIR:-${HOMELAB_STATE_DIR}/tofu-state-backups}" local latest section "Jeannie/OpenTofu State Restore Drill" latest="$(latest_file "${backup_dir}" 'tofu-state-*.tgz' || true)" if [[ -z "${latest}" ]]; then printf 'warn - no local OpenTofu state backup archive found in %s\n' "${backup_dir}" printf 'hint - run ./jeannie state-backup after important changes\n' return 0 fi tar -tzf "${latest}" >/dev/null printf 'ok - state archive can be listed: %s\n' "${latest}" } drill_gitops() { section "GitOps Rebuild Drill" git -C "${REPO_ROOT}" status --short git -C "${REPO_ROOT}" rev-parse --verify HEAD >/dev/null printf 'ok - repository HEAD: %s\n' "$(git -C "${REPO_ROOT}" rev-parse --short HEAD)" if command -v kubectl >/dev/null 2>&1 && [[ -s "${KUBECONFIG}" ]]; then kubectl --kubeconfig "${KUBECONFIG}" -n argocd get applications.argoproj.io 2>/dev/null || true else printf 'info - kubeconfig unavailable; skipping Argo CD application list\n' fi } usage() { cat <<'EOF' Usage: scripts/restore-drill {all|gitea|pihole|state|gitops} Read-only restore drills. These checks prove that restore inputs exist and can be inspected, but they do not replace running services or mutate Kubernetes. EOF } main() { case "${1:-all}" in all) drill_gitea drill_pihole drill_state drill_gitops ;; gitea) drill_gitea ;; pihole) drill_pihole ;; state) drill_state ;; gitops) drill_gitops ;; -h | --help | help) usage ;; *) usage >&2 return 2 ;; esac } main "$@"