#!/usr/bin/env bash set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "${repo_root}" usage() { cat >&2 <<'USAGE' usage: tofu.sh ACTION [ARGS...] Actions: init migrate BACKUP migrate-auth state [local] unlock LOCK_ID test lock test restore BACKUP test auth-migration test backend-failure plan apply output NAME USAGE exit 2 } require_args() { local expected="$1" shift [[ "$#" -eq "${expected}" ]] || usage } state_list_file() { (cd / && tofu state list -state="$1") } migrate_state() ( require_args 1 "$@" local backup="$1" before after before="$(mktemp)" after="$(mktemp)" trap 'rm -f -- "${before}" "${after}"' EXIT state_list_file "${repo_root}/terraform.tfstate" >"${before}" [[ ! -e "${backup}" ]] || { echo "backup already exists: ${backup}" >&2 exit 1 } install -m 600 "${repo_root}/terraform.tfstate" "${backup}" cmp -s "${repo_root}/terraform.tfstate" "${backup}" tofu init -migrate-state tofu state list >"${after}" diff -u "${before}" "${after}" ) migrate_auth() ( require_args 0 "$@" local target lock temporary target="${repo_root}/auth.auto.tfvars.json" lock="${repo_root}/.auth.auto.tfvars.lock" [[ ! -e "${target}" ]] || { echo "target already exists: ${target}" >&2 exit 1 } exec 9>"${lock}" flock -x 9 temporary="$(mktemp "${repo_root}/.auth-migration.XXXXXX")" trap 'rm -f -- "${temporary}"' EXIT printf '%s\n' 'nonsensitive(jsonencode({zot_htpasswd_content=var.zot_htpasswd_content,hallucygenie_basic_auth_users=var.hallucygenie_basic_auth_users}))' | tofu console -no-color | jq -er . >"${temporary}" jq -e 'type == "object" and has("zot_htpasswd_content") and has("hallucygenie_basic_auth_users")' "${temporary}" >/dev/null chmod 600 "${temporary}" mv "${temporary}" "${target}" echo "migrated auth variables to ${target}" echo "remove zot_htpasswd_content and hallucygenie_basic_auth_users from secrets.auto.tfvars, then run just plan" ) delete_test_objects() { local key="$1" wrangler r2 object delete "bugabinga-net-tofu-state/${key}.tflock" --remote >/dev/null 2>&1 || true wrangler r2 object delete "bugabinga-net-tofu-state/${key}" --remote >/dev/null 2>&1 || true } test_lock() ( local id key directory lock output id="$(cat /proc/sys/kernel/random/uuid)" key="lock-tests/${id}/terraform.tfstate" directory="$(mktemp -d)" lock="$(mktemp)" output="$(mktemp)" trap 'delete_test_objects "${key}"; rm -rf -- "${directory}"; rm -f -- "${lock}" "${output}"' EXIT cp backend.tf "${directory}/backend.tf" printf 'resource "terraform_data" "lock_probe" { input = "probe" }\n' >"${directory}/main.tf" (cd "${directory}" && tofu init -reconfigure -backend-config="key=${key}") >/dev/null printf '{"ID":"%s","Operation":"OperationTypeApply","Info":"lock contention test","Who":"just tofu test lock","Version":"%s","Created":"%s","Path":"%s"}\n' \ "${id}" "$(tofu version -json | jq -r .terraform_version)" "$(date --utc +%Y-%m-%dT%H:%M:%S.%NZ)" "${key}" >"${lock}" wrangler r2 object put "bugabinga-net-tofu-state/${key}.tflock" --remote --file "${lock}" >/dev/null if (cd "${directory}" && tofu apply --auto-approve --input=false --refresh=false --lock-timeout=0s) >"${output}" 2>&1; then cat "${output}" >&2 echo "lock contention was not rejected" >&2 exit 1 fi grep -F "${id}" "${output}" >/dev/null echo "lock contention rejected: ${id}" ) test_restore() ( require_args 1 "$@" local backup="$1" id key directory before after [[ -f "${backup}" ]] || { echo "backup not found: ${backup}" >&2 exit 1 } id="$(cat /proc/sys/kernel/random/uuid)" key="recovery-tests/${id}/terraform.tfstate" directory="$(mktemp -d)" before="$(mktemp)" after="$(mktemp)" trap 'delete_test_objects "${key}"; rm -rf -- "${directory}"; rm -f -- "${before}" "${after}"' EXIT cp backend.tf "${directory}/backend.tf" state_list_file "${backup}" >"${before}" wrangler r2 object put "bugabinga-net-tofu-state/${key}" --remote --file "${backup}" >/dev/null (cd "${directory}" && tofu init -reconfigure -backend-config="key=${key}") >/dev/null (cd "${directory}" && tofu state list) >"${after}" diff -u "${before}" "${after}" echo "backup restored and verified: ${id}" ) test_auth_migration() ( local directory output directory="$(mktemp -d)" output="$(mktemp)" trap 'rm -rf -- "${directory}"; rm -f -- "${output}"' EXIT cat >"${directory}/main.tf" <<'HCL' variable "zot_htpasswd_content" { type = string sensitive = true } variable "hallucygenie_basic_auth_users" { type = string sensitive = true } HCL cat >"${directory}/fixture.auto.tfvars.json" <<'JSON' { "zot_htpasswd_content": "alice:$2a$12$fixture\n", "hallucygenie_basic_auth_users": "alice $2a$14$fixture\n" } JSON ( cd "${directory}" printf '%s\n' 'nonsensitive(jsonencode({zot_htpasswd_content=var.zot_htpasswd_content,hallucygenie_basic_auth_users=var.hallucygenie_basic_auth_users}))' | tofu console -no-color | jq -er . ) >"${output}" jq -e ' .zot_htpasswd_content == "alice:$2a$12$fixture\n" and .hallucygenie_basic_auth_users == "alice $2a$14$fixture\n" ' "${output}" >/dev/null echo "auth migration conversion valid" ) test_backend_failure() ( local directory output directory="$(mktemp -d)" output="$(mktemp)" trap 'rm -rf -- "${directory}"; rm -f -- "${output}"' EXIT sed 's#https://c9526445475a4d62fd6e330810573ef0.r2.cloudflarestorage.com#http://127.0.0.1:1#' backend.tf >"${directory}/backend.tf" if (cd "${directory}" && AWS_ACCESS_KEY_ID=invalid AWS_SECRET_ACCESS_KEY=invalid AWS_MAX_ATTEMPTS=1 tofu init -reconfigure) >"${output}" 2>&1; then cat "${output}" >&2 echo "unavailable backend unexpectedly initialized" >&2 exit 1 fi grep -F '127.0.0.1:1' "${output}" >/dev/null [[ ! -e "${directory}/terraform.tfstate" ]] echo "unavailable backend failed closed" ) action="${1:-}" [[ -n "${action}" ]] || usage shift case "${action}" in init) require_args 0 "$@" tofu init ;; migrate) migrate_state "$@" ;; migrate-auth) migrate_auth "$@" ;; state) if [[ "${1:-}" == "local" ]]; then require_args 1 "$@" state_list_file "${repo_root}/terraform.tfstate" else require_args 0 "$@" tofu state list fi ;; unlock) require_args 1 "$@" tofu force-unlock "$1" ;; test) test_name="${1:-}" [[ -n "${test_name}" ]] || usage shift case "${test_name}" in lock) require_args 0 "$@" test_lock ;; restore) test_restore "$@" ;; auth-migration) require_args 0 "$@" test_auth_migration ;; backend-failure) require_args 0 "$@" test_backend_failure ;; *) usage ;; esac ;; plan) require_args 0 "$@" tofu plan --input=false --out=tfplan ;; apply) require_args 0 "$@" tofu apply --auto-approve -parallelism=1 tfplan ;; output) require_args 1 "$@" tofu output -raw "$1" ;; *) usage ;; esac