Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

scripts/local/tofu.sh

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