#!/usr/bin/env bash
# Generated by StackToIT from execution-plan.json. StackToIT has not run this script and runs none: it creates, changes and deletes nothing in any cloud.
#
# The one entry point of the execution package: it carries the steps of the plan out in the order of their dependencies, from PREPARE to VERIFY, and stops at the first step
# that a machine must not pass. Run it from anywhere:
#
#   bash execution/deploy.sh             the same as --dry-run
#   bash execution/deploy.sh --dry-run   prints the order and what would stop it; calls, applies and writes nothing, and needs no tool and no credential
#   bash execution/deploy.sh --run       carries the steps out
#
# What --run does:
#   - an OpenTofu stage: init, plan, the plan printed, an approval, apply, then a check that the state holds its blocks. A stage is one OpenTofu root; -target is never used;
#   - a STACKIT API step: the checks before it (no decision open, its values given, every step it comes after recorded), its outputs from `tofu output -json` into the
#     environment of the script (never copied by hand), the script of the step, which makes the call and the check after it when the plan states one;
#   - a step that waits for a decision, a value, a manual step, a data move or the cutover: it stops, says which and what closes it, and exits 10.
#
# Progress is the journal of the run, review/progress.yaml: the same file a person records in. A run skips what is recorded DONE or WAIVED for the step as it stands, so running
# the script again resumes and a step that holds never runs twice. To pass a manual step, a person records it there (state: DONE, and for a data move the result of its checks).
#
# No secret in a file: credentials come from the environment or the store of your CI. The plan of a stage can hold sensitive values, so it lies in a private temporary folder
# that is removed when the script ends.
#
# Prerequisites of --run: OpenTofu, jq, curl, awk; review/backend.tf, filled in from execution/backend.tf.template (the remote state in STACKIT Object Storage); the credentials of
# the STACKIT provider (STACKIT_SERVICE_ACCOUNT_KEY_PATH), of the calls (STACKIT_ACCESS_TOKEN) and of the state (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or
# STACKIT_BACKEND_ACCESS_KEY_ID and STACKIT_BACKEND_SECRET_ACCESS_KEY when the migrated estate's S3 calls use the first two).
#
# Without a terminal: STACKIT_DEPLOY_APPROVED_PLAN holds the digest of the plan a person approved, STACKIT_CONFIRM_ACTION the id of a step whose call needs a confirmation.
# Also read: STACKIT_DEPLOY_PROGRESS (another journal file), STACKIT_DEPLOY_BACKEND (another backend file).
#
# Exit codes: 0 the flow is complete (or a dry run ended); 10 it stopped for a person, a decision, a value or an approval; 1 a step failed; 2 a wrong argument.
# This plan: 66 items of the flow, up to the verification; 3 decommission steps after it stay with a person.
# No step sits between OpenTofu blocks, so the draft is one stage: terraform/.
set -euo pipefail

usage() {
  printf '%s\n' 'Usage: bash execution/deploy.sh [--dry-run | --run]' \
    '  --dry-run  print the order and what would stop it; nothing is called, applied or written (the default)' \
    '  --run      carry the steps out, up to the first step that holds' \
    'The comments at the head of this script say what each does and what it needs.'
}

mode=dry-run
asked=''
for arg in "$@"; do
  case "$arg" in
    --dry-run|--run)
      if [ -n "$asked" ] && [ "$asked" != "$arg" ]; then
        printf '%s\n' 'Give --dry-run or --run, not both.' >&2
        exit 2
      fi
      asked="$arg"
      mode="${arg#--}";;
    -h|--help) usage; exit 0;;
    *) printf 'Unknown argument: %s\n' "$arg" >&2; usage >&2; exit 2;;
  esac
done

here="${BASH_SOURCE[0]%/*}"
if [ "$here" = "${BASH_SOURCE[0]}" ]; then here=.; fi
here="$(CDPATH='' cd -- "$here" && pwd)"
out="${here%/*}"
journal="${STACKIT_DEPLOY_PROGRESS:-$out/review/progress.yaml}"
template="$out/progress-template.yaml"
backend="${STACKIT_DEPLOY_BACKEND:-$out/review/backend.tf}"
lock="$out/review/.deploy.lock"

work=''
locked=''
rows=''
rec=''
first_stop=''
stops=0
done_steps=0
would_steps=0
inited=''

cleanup() {
  if [ -n "$work" ]; then rm -rf -- "$work"; fi
  if [ -n "$locked" ]; then rmdir -- "$lock" 2>/dev/null || true; fi
}
trap cleanup EXIT

# --- the journal -------------------------------------------------------------------------------------------------------------------------------------------------------

# entries prints one tab-separated row of each entry of a journal: the step, its contract, its state, the confirmed contract and how many of its checks are not PASS or WAIVED.
entries() {
  awk '
    function value(s) {
      sub(/^[^:]*:[ \t]*/, "", s)
      if (substr(s, 1, 1) == "\"") {
        s = substr(s, 2)
        sub(/"[ \t]*(#.*)?$/, "", s)
        gsub(/\\"/, "\"", s)
        gsub(/\\\\/, "\\", s)
        return s
      }
      sub(/[ \t]+#.*$/, "", s)
      sub(/[ \t]+$/, "", s)
      return s
    }
    function flush() {
      if (id != "") printf "%s\t%s\t%s\t%s\t%d\n", id, (contract == "" ? "-" : contract), (state == "" ? "NOT_STARTED" : state), (confirmed == "" ? "-" : confirmed), open
      id = ""
    }
    /^[ \t]*- action_id:/ { flush(); id = value($0); contract = "-"; state = "NOT_STARTED"; confirmed = "-"; open = 0; next }
    /^[ \t]+contract_sha256:/ { contract = value($0); next }
    /^[ \t]+state:/ { state = value($0); next }
    /^[ \t]+content_confirmed_sha256:/ { confirmed = value($0); next }
    /^[ \t]+result:/ { r = value($0); if (r != "PASS" && r != "WAIVED") open++; next }
    END { flush() }
  ' "$1"
}

read_journal() {
  rows=''
  if [ -f "$journal" ]; then
    rows="$(entries "$journal")"
  elif [ -f "$template" ]; then
    rows="$(entries "$template")"
  fi
}

# recorded sets rec to the state of a step as the journal holds it for the step as it stands: DONE, WAIVED, NOT_STARTED, BLOCKED, IN_PROGRESS, FAILED, and besides them STALE (the
# step has changed since the entry), UNCONFIRMED (a DONE whose content a person has not confirmed) and CHECKS_OPEN (a DONE with a check that is not PASS or WAIVED).
recorded() {
  local id="$1" contract="$2" flags="${3:-}" row f_id f_contract f_state f_confirmed f_open
  rec=NOT_STARTED
  row="$(STEP="$id" awk -F'\t' '$1 == ENVIRON["STEP"] { print; exit }' <<< "$rows")"
  if [ -z "$row" ]; then return 0; fi
  IFS=$'\t' read -r f_id f_contract f_state f_confirmed f_open <<< "$row"
  : "$f_id"
  rec="$f_state"
  if [ "$f_state" = NOT_STARTED ]; then return 0; fi
  if [ "$f_contract" != "$contract" ]; then rec=STALE; return 0; fi
  if [ "$f_state" = DONE ]; then
    case ",$flags," in
      *,unbound,*) if [ "$f_confirmed" != "$contract" ]; then rec=UNCONFIRMED; return 0; fi;;
    esac
    if [ "$f_open" != 0 ]; then rec=CHECKS_OPEN; fi
  fi
}

# record writes a state for the steps listed in the file $work/targets (id, tab, contract) into the journal, in one replacement of the file. An entry the journal does not
# hold is added. Every other line stays as it is.
record() {
  local state="$1" note="$2"
  STATE="$state" NOTE="$note" AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" TARGETS="$work/targets" awk '
    function value(s) {
      sub(/^[^:]*:[ \t]*/, "", s)
      if (substr(s, 1, 1) == "\"") {
        s = substr(s, 2)
        sub(/"[ \t]*(#.*)?$/, "", s)
        gsub(/\\"/, "\"", s)
        gsub(/\\\\/, "\\", s)
      }
      return s
    }
    function scalar(s) {
      gsub(/\\/, "\\\\", s)
      gsub(/"/, "\\\"", s)
      return "\"" s "\""
    }
    BEGIN {
      while ((getline line < ENVIRON["TARGETS"]) > 0) {
        n = index(line, "\t")
        want[substr(line, 1, n - 1)] = substr(line, n + 1)
      }
    }
    /^[ \t]*- action_id:/ {
      cur = value($0)
      hit = (cur in want)
      if (hit) { seen[cur] = 1; match($0, /^[ \t]*/); pad = substr($0, 1, RLENGTH) "  " }
      print
      next
    }
    hit && index($0, pad "contract_sha256:") == 1 { print pad "contract_sha256: " scalar(want[cur]); next }
    hit && index($0, pad "state:") == 1 {
      print pad "state: " ENVIRON["STATE"]
      print pad "at: " scalar(ENVIRON["AT"])
      print pad "note: " scalar(ENVIRON["NOTE"])
      next
    }
    hit && (index($0, pad "at:") == 1 || index($0, pad "note:") == 1) { next }
    { print }
    END {
      for (id in want) {
        if (!(id in seen)) {
          print "  - action_id: " scalar(id)
          print "    contract_sha256: " scalar(want[id])
          print "    state: " ENVIRON["STATE"]
          print "    at: " scalar(ENVIRON["AT"])
          print "    note: " scalar(ENVIRON["NOTE"])
        }
      }
    }
  ' "$journal" > "$journal.new"
  mv -- "$journal.new" "$journal"
  read_journal
}

# record_one records one step.
record_one() {
  printf '%s\t%s\n' "$1" "$2" > "$work/targets"
  record "$3" "$4"
}

# --- what the flow says ------------------------------------------------------------------------------------------------------------------------------------------------

say() { printf '%s\n' "$*"; }

# sentence ends a text as a sentence.
sentence() {
  case "$1" in
    *.) printf '%s' "$1";;
    *) printf '%s.' "$1";;
  esac
}

# show prints one line of the flow: the number of the step in the plan, what happens to it, and its words.
show() { printf '  %5s  %-14s %s\n' "$1" "$2" "$3"; }

# stop_at ends a run, or marks the first stop of a dry run.
stop_at() {
  stops=$((stops + 1))
  if [ "$mode" = run ]; then
    printf '\nSTOP: %s\n' "$1"
    exit 10
  fi
  if [ -z "$first_stop" ]; then first_stop="$1"; fi
}

fail() {
  printf '\nFAILED: %s\n' "$1" >&2
  exit 1
}

phase() { printf '\n%s\n' "$1"; }

note() { printf '  %s\n' "$1"; }

stop_flow() {
  printf '\n  %s\n' "$1"
  stop_at "$1"
}

# --- manual steps ------------------------------------------------------------------------------------------------------------------------------------------------------

# person is a step only a person passes.
person() {
  local n="$1" id="$2" contract="$3" flags="$4" hold="$5" words="$6" where="$7" kind="$8" tag how said
  said="$(sentence "$words")"
  if [ -n "$hold" ]; then
    show "$n" 'waits' "$words"
    stop_at "Step $n ($id): $hold. Answer it in review/decisions.yaml and run StackToIT again, which writes this script anew. The step: $said"
    return 0
  fi
  recorded "$id" "$contract" "$flags"
  case "$rec" in
    DONE|WAIVED)
      show "$n" 'done' "$words"
      done_steps=$((done_steps + 1))
      return 0;;
  esac
  tag="$kind"
  how="a step for a person"
  case ",$flags," in
    *,cutover,*) tag='cutover'; how="part of the cutover, which this script never passes by itself";;
    *,data,*) tag='data move'; how="part of a data move, which this script never passes by itself";;
  esac
  show "$n" "$tag" "$words"
  case "$rec" in
    STALE) stop_at "Step $n ($id) was recorded and has changed since: check it as it stands, then copy its contract_sha256 from progress-template.yaml to review/progress.yaml and record it again. The step: $said";;
    UNCONFIRMED) stop_at "Step $n ($id) is recorded DONE and the plan cannot check its content: a person confirms it with the line content_confirmed_sha256 in review/progress.yaml. The step: $said";;
    CHECKS_OPEN) stop_at "Step $n ($id) is recorded DONE and a check of it is not PASS or WAIVED in review/progress.yaml. The step: $said";;
    FAILED) stop_at "Step $n ($id) is recorded FAILED in review/progress.yaml. When its cause is removed, record it again. The step: $said";;
    *)
      if [ "$kind" = refused ]; then
        stop_at "Step $n ($id) is refused: nothing on the target carries it out. The step: $said A person records state: WAIVED for it, with the reason, in review/progress.yaml and runs this script again."
      else
        stop_at "Step $n ($id) is $how. The step: $said Where: ${where:-execution/manual/actions.md}. When it is done, record state: DONE for it in review/progress.yaml and run this script again."
      fi;;
  esac
}

manual() { person "$1" "$2" "$3" "$4" "$5" "$6" "$7" 'manual'; }

# refuse is a step the plan does not carry out: nothing on the target does it. A person records it WAIVED, with the reason, to go on.
refuse() { person "$1" "$2" "$3" "$4" "$5" "$6" "$7" 'refused'; }

# --- values and tools --------------------------------------------------------------------------------------------------------------------------------------------------

# missing_values prints the names of the list whose TF_VAR_ variable is not set.
missing_values() {
  local name var gone=''
  for name in $1; do
    var="TF_VAR_$name"
    if [ -z "${!var:-}" ]; then gone="$gone $name"; fi
  done
  printf '%s' "${gone# }"
}

# tf runs OpenTofu, with the keys of the state bucket when they are given apart.
tf() {
  if [ -n "${STACKIT_BACKEND_ACCESS_KEY_ID:-}" ]; then
    AWS_ACCESS_KEY_ID="$STACKIT_BACKEND_ACCESS_KEY_ID" AWS_SECRET_ACCESS_KEY="${STACKIT_BACKEND_SECRET_ACCESS_KEY:-}" tofu "$@"
  else
    tofu "$@"
  fi
}

# ready_root puts the backend beside a root and initialises it, once in a run.
ready_root() {
  local root="$1" dir="$out/$1"
  case " $inited " in *" $root "*) return 0;; esac
  if [ -f "$dir/backend.tf" ] && ! cmp -s "$backend" "$dir/backend.tf"; then
    fail "$root/backend.tf differs from $backend: all the roots use one state, so they use one backend. Remove the file, or give the same one."
  fi
  cp -- "$backend" "$dir/backend.tf"
  tf -chdir="$dir" init -input=false -no-color
  inited="$inited $root"
}

sha256_of() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum | awk '{ print $1 }'
  else
    shasum -a 256 | awk '{ print $1 }'
  fi
}

# --- OpenTofu stages ---------------------------------------------------------------------------------------------------------------------------------------------------

stage_number=0
stage_dir=''
stage_kind=''
stage_hold=''
stage_values=''
stage_words=''
stage_cutover=''
stage_ids=()
stage_contracts=()
stage_blocks=''

stage_open() {
  stage_number="$1"; stage_dir="$2"; stage_kind="$3"; stage_hold="$4"; stage_values="$5"; stage_words="$6"; stage_cutover="$7"
  stage_ids=()
  stage_contracts=()
  stage_blocks=''
}

tf_step() {
  stage_ids+=("$1")
  stage_contracts+=("$2")
  stage_blocks="$stage_blocks $3"
}

# stage_targets writes the steps of the stage to the file record reads.
stage_targets() {
  local i
  : > "$work/targets"
  for i in "${!stage_ids[@]}"; do
    printf '%s\t%s\n' "${stage_ids[$i]}" "${stage_contracts[$i]}" >> "$work/targets"
  done
}

# var_files gives a stage before the last the values the draft reads by itself: OpenTofu reads them only from the folder of a root.
var_files() {
  local f
  vars=()
  if [ "$stage_kind" = whole ]; then return 0; fi
  for f in "$out"/terraform/terraform.tfvars "$out"/terraform/*.auto.tfvars "$out"/terraform/*.auto.tfvars.json; do
    if [ -f "$f" ]; then vars+=("-var-file=$f"); fi
  done
}

# approve lets a plan through: the digest a person approved, or the word of a person at a terminal.
approve() {
  local digest="$1" answer=''
  if [ "${STACKIT_DEPLOY_APPROVED_PLAN:-}" = "$digest" ]; then
    say "The plan is the one approved in STACKIT_DEPLOY_APPROVED_PLAN ($digest)."
    return 0
  fi
  if [ -t 0 ]; then
    printf 'Apply this plan (%s)? Type apply to go on, anything else stops here: ' "$digest"
    read -r answer || answer=''
    if [ "$answer" = apply ]; then return 0; fi
  fi
  stop_at "$stage_words: the plan above waits for an approval. Its digest is $digest (the addresses and the actions of the plan, no value): a person reads it and types apply at a terminal, or sets STACKIT_DEPLOY_APPROVED_PLAN to the digest and runs this script again."
}

run_stage() {
  local dir="$out/$stage_dir" plan="$work/stage-$stage_number.tfplan" rc=0 changes digest removes held state address
  ready_root "$stage_dir"
  var_files
  tf -chdir="$dir" plan -input=false -no-color -detailed-exitcode -out="$plan" ${vars[@]+"${vars[@]}"} || rc=$?
  case "$rc" in
    0)
      stage_targets
      record DONE "execution/deploy.sh: OpenTofu plans no change in $stage_dir"
      show '' 'no change' "$stage_words"
      return 0;;
    2) ;;
    *) fail "$stage_words: tofu plan failed (exit $rc). Nothing was applied. Run this script again when its cause is removed.";;
  esac
  changes="$(tf -chdir="$dir" show -json "$plan" | jq -c '[.resource_changes[]? | select(.change.actions != ["no-op"]) | [.address, .change.actions]] | sort')"
  digest="$(printf '%s %s' "$stage_dir" "$changes" | sha256_of)"
  removes="$(jq '[.[] | select(.[1] | index("delete"))] | length' <<< "$changes")"
  if [ "$removes" != 0 ] && [ "$stage_kind" != whole ]; then
    fail "$stage_words: the plan deletes or replaces $removes objects. A stage before the last only adds to the state, so it is applied before the rest of the draft or not at all: nothing was applied."
  fi
  if [ "$removes" != 0 ]; then
    say "This plan deletes or replaces $removes objects: read them before you approve."
  fi
  if [ -n "$stage_cutover" ]; then say "$stage_cutover"; fi
  approve "$digest"
  stage_targets
  record IN_PROGRESS "execution/deploy.sh: tofu apply of $stage_dir started"
  rc=0
  tf -chdir="$dir" apply -input=false -no-color "$plan" || rc=$?
  if [ "$rc" != 0 ]; then
    record FAILED "execution/deploy.sh: tofu apply of $stage_dir failed (exit $rc); running the script again plans what is left"
    fail "$stage_words: tofu apply failed (exit $rc). The journal says so; running this script again plans what is left."
  fi
  state="$(tf -chdir="$dir" state list -no-color)"
  held=''
  for address in $stage_blocks; do
    if ! grep -Fxq -- "$address" <<< "$state"; then held="$held $address"; fi
  done
  if [ -n "$held" ]; then
    record FAILED "execution/deploy.sh: the state does not hold$held after the apply of $stage_dir"
    fail "$stage_words: the state does not hold$held after the apply."
  fi
  record DONE "execution/deploy.sh: applied $stage_dir, the state holds its blocks"
  show '' 'applied' "$stage_words"
}

stage_go() {
  local i all=1 gone
  for i in "${!stage_ids[@]}"; do
    recorded "${stage_ids[$i]}" "${stage_contracts[$i]}"
    case "$rec" in
      DONE|WAIVED) ;;
      *) all=0;;
    esac
  done
  printf '\n'
  if [ "$stage_kind" = whole ]; then
    say "OpenTofu: the whole draft"
  else
    say "OpenTofu: a stage before the rest of the draft"
  fi
  if [ "$all" = 1 ]; then
    show '' 'done' "$stage_words"
    done_steps=$((done_steps + ${#stage_ids[@]}))
    return 0
  fi
  if [ -n "$stage_hold" ]; then
    show '' 'waits' "$stage_words"
    stop_at "$stage_words: $stage_hold. Answer in review/decisions.yaml and run StackToIT again, which writes this script anew. OpenTofu applies a root whole, so no block of this stage is applied before."
    return 0
  fi
  gone="$(missing_values "$stage_values")"
  if [ -n "$gone" ]; then
    show '' 'waits' "$stage_words"
    stop_at "$stage_words: no value for: $gone. Give each in the environment as TF_VAR_<name>, or in review/target-values.tfvars and run StackToIT again."
    return 0
  fi
  if [ "$mode" != run ]; then
    show '' 'would apply' "$stage_words"
    if [ -n "$stage_cutover" ]; then note "$stage_cutover"; fi
    would_steps=$((would_steps + ${#stage_ids[@]}))
    return 0
  fi
  show '' 'plans' "$stage_words"
  run_stage
}

# --- STACKIT API steps -------------------------------------------------------------------------------------------------------------------------------------------------

# confirmed passes a call that asks for a confirmation.
confirmed() {
  local id="$1" words="$2" answer=''
  if [ "${STACKIT_CONFIRM_ACTION:-}" = "$id" ]; then return 0; fi
  if [ -t 0 ]; then
    printf 'This call deletes on the target, or is not safe to run twice: %s\nType run to make it, anything else stops here: ' "$words"
    read -r answer || answer=''
    if [ "$answer" = run ]; then return 0; fi
  fi
  stop_at "Step $id deletes on the target, or is not safe to run twice: its script says what to check. A person types run at a terminal, or sets STACKIT_CONFIRM_ACTION to the id of the step and runs this script again."
}

api() {
  local n="$1" id="$2" contract="$3" flags="$4" hold="$5" values="$6" root="$7" script="$8" words="$9" gone json pair var name value said rc=0
  shift 9
  said="$(sentence "$words")"
  if [ -n "$hold" ]; then
    show "$n" 'waits' "$words"
    stop_at "Step $n ($id): $hold. The step: $said"
    return 0
  fi
  recorded "$id" "$contract"
  case "$rec" in
    DONE|WAIVED)
      show "$n" 'done' "$words"
      done_steps=$((done_steps + 1))
      return 0;;
  esac
  case ",$flags," in
    *,person,*)
      show "$n" 'a person runs' "$words"
      stop_at "Step $n ($id) is part of the cutover or of a data move, which this script never passes by itself. A person runs bash $script and records state: DONE for it in review/progress.yaml. The step: $said"
      return 0;;
  esac
  case ",$flags," in
    *,once,*)
      case "$rec" in
        IN_PROGRESS|FAILED|STALE)
          show "$n" 'a person checks' "$words"
          stop_at "Step $n ($id) is not safe to run twice and is recorded $rec: whether it ran is not known. A person checks, and records it in review/progress.yaml. The step: $said"
          return 0;;
      esac;;
  esac
  gone="$(missing_values "$values")"
  if [ -n "$gone" ]; then
    show "$n" 'waits' "$words"
    stop_at "Step $n ($id) has no value for: $gone. Give each in the environment as TF_VAR_<name>. The step: $said"
    return 0
  fi
  if [ "$mode" != run ]; then
    show "$n" 'would call' "$words"
    would_steps=$((would_steps + 1))
    return 0
  fi
  show "$n" 'calls' "$words"
  case ",$flags," in *,confirm,*) confirmed "$id" "$words";; esac
  if [ $# -gt 0 ]; then
    ready_root "$root"
    json="$(tf -chdir="$out/$root" output -json -no-color)"
  fi
  case ",$flags," in *,secret-output,*) ready_root terraform;; esac
  record_one "$id" "$contract" IN_PROGRESS "execution/deploy.sh: the call of this step started"
  (
    for pair in "$@"; do
      var="${pair%%=*}"
      name="${pair#*=}"
      if [ -n "${!var:-}" ]; then continue; fi
      value="$(OUTPUT="$name" jq -er '.[env.OUTPUT] | if .sensitive then error("sensitive") else .value end | if type == "string" then . else error("no text") end' <<< "$json")" || exit 9
      export "${var?}=$value"
    done
    case ",$flags," in *,confirm,*) export STACKIT_CONFIRM_ACTION="$id";; esac
    exec bash "$out/$script"
  ) || rc=$?
  case "$rc" in
    0)
      record_one "$id" "$contract" DONE "execution/deploy.sh: the script of this step answered 0; a check the plan leaves to a person stays in execution/verify/checks.md"
      show "$n" 'called' "$words";;
    3|4|5|9)
      record_one "$id" "$contract" NOT_STARTED "execution/deploy.sh: the script stopped before the call (exit $rc)"
      if [ "$rc" = 9 ]; then
        stop_at "Step $n ($id): an output of OpenTofu the step reads is not in $root, is sensitive or is no text. Nothing was called."
      else
        stop_at "Step $n ($id): the script stopped before the call (exit $rc): its message is above. Nothing was called."
      fi;;
    *)
      record_one "$id" "$contract" FAILED "execution/deploy.sh: the script of this step ended with exit $rc"
      fail "Step $n ($id): the script ended with exit $rc: its message is above.";;
  esac
}

# --- the end -----------------------------------------------------------------------------------------------------------------------------------------------------------

finish() {
  printf '\n'
  if [ "$1" != 0 ]; then
    say "After the verification, $1 decommission steps stay with a person: execution/manual/actions.md. This script does not retire the source."
  fi
  if [ "$2" != 0 ]; then
    say "$2 steps are left out: an answered decision took another path."
  fi
}

# prerequisites says what a run needs and what is here. It names variables and never prints a value.
prerequisites() {
  local tool name
  say "What a run needs, as things stand here:"
  for tool in tofu jq curl awk; do
    if command -v "$tool" >/dev/null 2>&1; then say "  tool $tool: found"; else say "  tool $tool: NOT FOUND"; fi
  done
  if [ -f "$backend" ]; then say "  backend: $backend"; else say "  backend: NOT FOUND: copy execution/backend.tf.template to review/backend.tf and fill it in"; fi
  for name in STACKIT_SERVICE_ACCOUNT_KEY_PATH STACKIT_ACCESS_TOKEN; do
    if [ -n "${!name:-}" ]; then say "  $name: set"; else say "  $name: not set"; fi
  done
  if [ -n "${AWS_ACCESS_KEY_ID:-}${STACKIT_BACKEND_ACCESS_KEY_ID:-}" ]; then say "  keys of the state bucket: set"; else say "  keys of the state bucket: not set"; fi
}

# ready_run holds a run to its prerequisites, and takes the journal and the lock.
ready_run() {
  local tool
  for tool in tofu jq curl awk; do
    if ! command -v "$tool" >/dev/null 2>&1; then
      printf '%s\n' "$tool is not installed. Nothing was run." >&2
      exit 1
    fi
  done
  if [ ! -f "$backend" ]; then
    printf '\nSTOP: %s\n' "The remote state is a prerequisite: copy execution/backend.tf.template to review/backend.tf and fill it in, then run this script again. Nothing was run."
    exit 10
  fi
  if grep -q 'REPLACE_WITH' -- "$backend"; then
    printf '\nSTOP: %s\n' "$backend still holds a value to replace. Nothing was run."
    exit 10
  fi
  if [ -z "${AWS_ACCESS_KEY_ID:-}${STACKIT_BACKEND_ACCESS_KEY_ID:-}" ]; then
    printf '\nSTOP: %s\n' "Give the keys of the state bucket in the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or STACKIT_BACKEND_ACCESS_KEY_ID and STACKIT_BACKEND_SECRET_ACCESS_KEY). Nothing was run."
    exit 10
  fi
  mkdir -p -- "$out/review"
  if ! mkdir -- "$lock" 2>/dev/null; then
    printf '%s\n' "Another run holds $lock. If none does, remove the folder. Nothing was run." >&2
    exit 1
  fi
  locked=1
  if [ ! -f "$journal" ]; then cp -- "$template" "$journal"; fi
  work="$(mktemp -d)"
  chmod 700 "$work"
}

# same_environment stops a run on a journal written for another environment: a state it records is not the state of this one.
same_environment() {
  local own theirs
  if [ ! -f "$journal" ] || [ ! -f "$template" ]; then return 0; fi
  own="$(awk '/^environment_sha256:/ { print $2; exit }' "$template")"
  theirs="$(awk '/^environment_sha256:/ { print $2; exit }' "$journal")"
  if [ -n "$own" ] && [ -n "$theirs" ] && [ "$own" != "$theirs" ]; then
    stop_at "$journal was written for another environment than this run. After you checked the steps, copy environment_sha256 from progress-template.yaml into it."
  fi
}

# --- the plan ----------------------------------------------------------------------------------------------------------------------------------------------------------

the_plan() {
  phase 'PREPARE'
  manual 1 'MAN/identity/facts:google_cloud_run_v2_job.migrate:google_cloud_run_v2_job.migrate.template.0.template.0.service_account-8dc3e1' 'sha256:9ce88532216052b4662281e47d587f1e6be1072381afd25888149d071c7db193' '' '' 'the step "Decide how google_cloud_run_v2_job.migrate gets its application identity on…"' 'execution/manual/actions.md'
  manual 2 'MAN/identity/facts:google_cloud_run_v2_service.server:google_cloud_run_v2_service.server.template.0.service_account-5ccce9' 'sha256:f5dea1671537e56d536477a239fecd6215d0577e3f0b6d2651162578bc98f4e4' '' '' 'the step "Decide how google_cloud_run_v2_service.server gets its application identity on…"' 'execution/manual/actions.md'
  manual 3 'MAN/identity/held:principal:google_service_account.automation' 'sha256:f7ffb9286971eb16e5b22edd233e7275ac67b11d179c8f95faf2d816bb0a14f1' '' '' 'the step "Decide who may use the STACKIT service account written for…" for google_service_account.automation' 'execution/manual/actions.md'
  manual 4 'MAN/identity/held:principal:google_service_account.server' 'sha256:b2f8a119a8434fb22925ca5dcfd8dd7c316b6e42bbcd70b2366747af3b799e1e' '' '' 'the step "Decide who may use the STACKIT service account written for…" for google_service_account.server' 'execution/manual/actions.md'
  phase 'SECURITY'
  manual 5 'MAN/semantic/google_secret_manager_secret.django_admin_password/open-settings-security-architect' 'sha256:e4d022affed32f393b55396b4ee7cff94c1f05b58167a54105acff0f1b01b5b6' '' '' 'the review of the open settings of google_secret_manager_secret.django_admin_password' 'execution/manual/actions.md'
  manual 6 'MAN/semantic/google_secret_manager_secret.django_admin_password/secret-settings' 'sha256:df1f4f648c17778723aea45f6ff5beab01e05e576e780a3f8c3b7f6656ee7c6a' '' '' 'the setting secret-settings of google_secret_manager_secret.django_admin_password' 'execution/manual/actions.md'
  manual 7 'MAN/semantic/google_secret_manager_secret.django_settings/open-settings-security-architect' 'sha256:1bafb8a64674df75002410bbee6532ccca5fe8e3b9fb96d35d6963fd228d476b' '' '' 'the review of the open settings of google_secret_manager_secret.django_settings' 'execution/manual/actions.md'
  manual 8 'MAN/semantic/google_secret_manager_secret.django_settings/secret-settings' 'sha256:5429d4f14ca17f1771bec7cc62fc2ce5ef912981e660bdc1047f02d6d742c26c' '' '' 'the setting secret-settings of google_secret_manager_secret.django_settings' 'execution/manual/actions.md'
  manual 9 'MAN/semantic/google_service_account.automation/open-settings' 'sha256:efe342a02b7b1488c69a60117220f95f0f5b7837587b801f0f58374c32ee305d' '' '' 'the review of the open settings of google_service_account.automation' 'execution/manual/actions.md'
  manual 10 'MAN/semantic/google_service_account.client/open-settings' 'sha256:48c0dfdd36ae4a59ee1219de15f7e8c02659613c13037672dd5a92294a2999bd' '' '' 'the review of the open settings of google_service_account.client' 'execution/manual/actions.md'
  manual 11 'MAN/semantic/google_service_account.init/open-settings' 'sha256:82059bb9ede16c26244fa3bffb25dca575e0a252816240eaee433038947debcc' '' '' 'the review of the open settings of google_service_account.init' 'execution/manual/actions.md'
  manual 12 'MAN/semantic/google_service_account.server/open-settings' 'sha256:7af999d3747206c520a658131eebf706ea93abaa7aa99169283457a57248ed68' '' '' 'the review of the open settings of google_service_account.server' 'execution/manual/actions.md'
  stage_open 1 'terraform' 'whole' '' 'containers_machine_type containers_nodes_max containers_nodes_min media_bucket_name postgres_allowed_cidrs postgres_storage_size postgresflex_storage_class project_id rabbitmq_allowed_cidrs rabbitmq_plan_name rabbitmq_version' 'Stage 1 of 1: terraform, 10 OpenTofu steps (security, data services, platform services)' ''
  tf_step 'TF/stackit_secretsmanager_instance.migrated_secrets' 'sha256:bf4437463b183eb9edbf651a11a6c760a6f6e689cd4c4a314406dd01980dbe95' 'stackit_secretsmanager_instance.migrated_secrets'
  tf_step 'TF/stackit_secretsmanager_user.migration' 'sha256:e9934f2386740df500399a2b8ff480893d2ac891665b5d9ea42219ad9f315b56' 'stackit_secretsmanager_user.migration'
  tf_step 'TF/stackit_service_account.automation' 'sha256:31dbb833569c4f9b30fea9c6578bd970ef7772efaf21e5bff73b4a579d731da5' 'stackit_service_account.automation'
  tf_step 'TF/stackit_service_account.client' 'sha256:5177eb717a2ce5845ba31a46b8c9e8e015a27b0715aad0c1b63d9a41344058c0' 'stackit_service_account.client'
  tf_step 'TF/stackit_service_account.init' 'sha256:81bc70f2e3f7063208020b4d111f1057d99ee7ac3f874f52203983a0d2a69ecd' 'stackit_service_account.init'
  tf_step 'TF/stackit_service_account.server' 'sha256:9cafe9e335a20b39fb2c979beb3e842c468c53a1d5552f93559c781e2ae4b4f6' 'stackit_service_account.server'
  tf_step 'TF/stackit_objectstorage_bucket.media' 'sha256:3fd9fa0364d73f11295c62c3f61e67dce2bb1187ba0403c0edc7131e34970276' 'stackit_objectstorage_bucket.media'
  tf_step 'TF/stackit_postgresflex_instance.postgres' 'sha256:0e771974810184060fe13057cd835e66335dda735e70fca90b1bfc88f2ecfc39' 'stackit_postgresflex_instance.postgres'
  tf_step 'TF/stackit_rabbitmq_instance.messaging' 'sha256:23d196fb1506986e401cf479cb7c90aceb7dc0fbcc512ddef887cd07ef9b8cba' 'stackit_rabbitmq_instance.messaging'
  tf_step 'TF/stackit_ske_cluster.containers' 'sha256:14d10f4201b29dc7074758eb8d8e44f0fc8d0749e240f46112af780f038fb370' 'stackit_ske_cluster.containers'
  stage_go
  phase 'SECURITY'
  api 15 'API/secrets-manager-vault/WriteSecret/google_secret_manager_secret.django_admin_password' 'sha256:e779adfd8765c17c4647043906970e2d4e04fafcba754c78daca4ee440644fb4' 'secret-output' 'it waits for the decision DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password' '' 'terraform' 'execution/api/secrets-manager-vault/001-writesecret.sh' 'secrets-manager-vault WriteSecret (execution/api/secrets-manager-vault/001-writesecret.sh). Step state: generated, tested offline.' 'STACKIT_INPUT_INSTANCEID=stackit_secretsmanager_instance_migrated_secrets_instance_id' 'STACKIT_INPUT_USERNAME=stackit_secretsmanager_user_migration_username'
  api 16 'API/secrets-manager-vault/WriteSecret/google_secret_manager_secret.django_settings' 'sha256:b8dd9e6429418b7b234d929b61451912171fea974aabea87325405c00572b13d' 'secret-output' 'it waits for the decision DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password' '' 'terraform' 'execution/api/secrets-manager-vault/002-writesecret.sh' 'secrets-manager-vault WriteSecret (execution/api/secrets-manager-vault/002-writesecret.sh). Step state: generated, tested offline.' 'STACKIT_INPUT_INSTANCEID=stackit_secretsmanager_instance_migrated_secrets_instance_id' 'STACKIT_INPUT_USERNAME=stackit_secretsmanager_user_migration_username'
  manual 17 'MAN/path/secrets-migration/read-back' 'sha256:cf5055d6971aaaf057386f793d62015784df8ea1e90dc200a62a45937609651b' '' 'it waits for the decision DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password' 'the step "Read each migrated secret"' 'execution/manual/actions.md'
  manual 18 'MAN/path/secrets-migration/retire-user' 'sha256:bd48863b3841d52e33e7b681c067661406b2d626f91b7fbab6c51f3d2ea33ad9' '' 'it waits for the decision DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password' 'the step "Remove the migration user stackit_secretsmanager_user.migration of the Secrets…"' 'execution/manual/actions.md'
  phase 'DATA'
  manual 23 'MAN/estate/SOURCE_PROJECT_MAPPING' 'sha256:3787b0bd6c646865abd2d4830b74a85700eb97d3166c7b223cd92bbf5d83f408' '' 'it waits for the decision DEC-GEN-CONVERT-SOURCE-PROJECT-MAPPING' 'the review of the open settings of google_sql_database_instance.postgres "SOURCE_PROJECT_MAPPING"' 'execution/manual/actions.md'
  manual 24 'MAN/estate/SOURCE_REGION_MAPPING' 'sha256:cf0a95f3434ea7d2dff90570f5c0742caacd380af76700bdcb64ddbeee3ecabf' '' 'it waits for the decision DEC-GEN-CONVERT-SOURCE-REGION-MAPPING' 'the review of the open settings of google_cloud_run_v2_job.migrate, google_cloud_run_v2_service.server, google_sql_database_instance.postgres and 1 more' 'execution/manual/actions.md'
  manual 25 'MAN/semantic/google_sql_database_instance.postgres/database-users' 'sha256:479fe269cb156726d44f3bd13fc8499ce40ffe4d410e8b3ad4513a862094de28' '' '' 'the setting database-users of google_sql_database_instance.postgres' 'execution/manual/actions.md'
  manual 26 'MAN/semantic/google_sql_database_instance.postgres/open-settings-platform-architect' 'sha256:1e206538724178dfc6335248850bd6b7c610000abbf0ff0bce0164495ed7bd13' '' '' 'the review of the open settings of google_sql_database_instance.postgres "open-settings-platform-architect"' 'execution/manual/actions.md'
  manual 27 'MAN/semantic/google_sql_database_instance.postgres/user-and-database-settings' 'sha256:9d356b13251ae9e9e177120e962a63cd2318c1bfbef550af874f9c8494a20f2e' '' '' 'the setting user-and-database-settings of google_sql_database_instance.postgres' 'execution/manual/actions.md'
  manual 28 'MAN/semantic/google_storage_bucket.media/geo-replication' 'sha256:1d539fec2aa58572d1001dde4a28684246bcae17cc577d5d18557a54408383ec' '' '' 'the setting geo-replication of google_storage_bucket.media' 'execution/manual/actions.md'
  manual 29 'MAN/semantic/google_storage_bucket.media/iam-access' 'sha256:35c5ee960bbe2a6cb792e971a57b6d6596d051f01a053316dc12b240eb1dba48' '' '' 'the setting iam-access of google_storage_bucket.media' 'execution/manual/actions.md'
  manual 30 'MAN/semantic/google_storage_bucket.media/open-settings-identity-architect' 'sha256:e7acbc5e93086cfbd96918cf14100ab946d6a25b338c5b360d4c8685b2afc49a' '' '' 'the review of the open settings of google_storage_bucket.media' 'execution/manual/actions.md'
  manual 31 'MAN/semantic/google_storage_bucket.media/tc-storage-bucket-name' 'sha256:48ae38f0ec2a8b8980e09257b924009764da8b030f200ff5b85d309eae253b19' '' '' 'the setting "name" of google_storage_bucket.media' 'execution/manual/actions.md'
  manual 32 'MAN/unit/google_firebase_hosting_site.client' 'sha256:53b8bd455caf11c82062923d434ba0608d21b02d513ac864a3b4517b04448967' '' 'it waits for the decision DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_firebase_hosting_site.client' 'the proposed alternative of google_firebase_hosting_site.client' 'execution/manual/actions.md'
  refuse 33 'REF/semantic/google_storage_bucket.media/force-destroy' 'sha256:27ab2b1d19b5cc9852efb99d9f013af56fa05b2ea1114d542536c96c79d0a638' '' '' 'the refusal of the setting force-destroy of google_storage_bucket.media' 'execution/manual/actions.md'
  manual 35 'MAN/data/google_storage_bucket.media/preflight' 'sha256:be5707e12272cd31f95dae35217bb308b12a10a58644b5b0d376cc8b064de3a1' 'data' '' 'the data preflight of google_storage_bucket.media' 'terraform/handover/bucket-media/data-copy.md'
  manual 36 'MAN/data/google_storage_bucket.media/rehearsal' 'sha256:fb8f5653c1b27313c036a208da8e32b3b6e5036db02d20a827b4430543673953' 'data' '' 'the data rehearsal of google_storage_bucket.media' 'execution/manual/actions.md'
  manual 37 'MAN/data/google_storage_bucket.media/initial-copy/rclone-copy' 'sha256:b2f8230144fe20a2c31ea1f987ca72ca583cf9aa240232dba3a03a0c3157bbbc' 'data' '' 'the initial copy of the data of google_storage_bucket.media with rclone' 'execution/manual/actions.md'
  manual 38 'MAN/data/google_storage_bucket.media/delta-sync/rclone-copy' 'sha256:17c9310edc64cbf49e325fb93b12d756fc44c8fdba992b6787d229ac00456eb5' 'data' '' 'the replication watch of google_storage_bucket.media with rclone' 'execution/manual/actions.md'
  manual 40 'MAN/data/google_sql_database_instance.postgres/preflight' 'sha256:625dfa0074e758977d0f6544a0924e6f042092e75e54232ce3d05b119d793ce3' 'data' '' 'the data preflight of google_sql_database_instance.postgres' 'terraform/handover/database-postgres/data-move.md'
  manual 41 'MAN/data/google_sql_database_instance.postgres/rehearsal' 'sha256:c721c9f72c837e81f08495fed8d297b620f40285482685b4846a5a7da10ab519' 'data' '' 'the data rehearsal of google_sql_database_instance.postgres' 'execution/manual/actions.md'
  manual 42 'MAN/data/google_sql_database_instance.postgres/initial-copy/logical-replication' 'sha256:9975e45d05b8b951a2c376b33ed4d1135e667e127be98be6f37205d04000bdee' 'data' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the initial copy of the data of google_sql_database_instance.postgres by logical replication' 'execution/manual/actions.md'
  manual 43 'MAN/data/google_sql_database_instance.postgres/delta-sync/logical-replication' 'sha256:0a172aba592d05b774c193f31b6eefcd90857883cbf7e827e320bae4dd5d76e7' 'data' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the replication watch of google_sql_database_instance.postgres by logical replication' 'execution/manual/actions.md'
  phase 'PLATFORM'
  manual 44 'MAN/pubsub.topic/name/google_pubsub_topic.faux' 'sha256:c41287819c603e482146314e61c1229d4399319b08dee3a2d10702627c847cd5' '' 'it waits for the decision DEC-GEN-CONVERT-API-OPT-IN/google_pubsub_topic.faux' 'the step "Create the topic under a displayName of 1 to 36 letters, digits, spaces and…"' 'execution/manual/actions.md'
  manual 45 'MAN/rabbitmq/topology' 'sha256:15e687d6cadb4472ed17bbc923f37dad88fe773aca2f34b8ab3fe533fc652eae' '' '' 'the step "Declare the topics and subscriptions of the source on the RabbitMQ instance"' 'execution/manual/actions.md'
  manual 46 'MAN/semantic/google_cloud_run_v2_job.migrate/cloud-run-v2-job-network-exposure' 'sha256:1c9a1fe1e40e8c8828dfe74a5c1ec278d05e571e1b7281d0a78cb3d11dd09af9' '' '' 'the setting cloud-run-v2-job-network-exposure of google_cloud_run_v2_job.migrate' 'execution/manual/actions.md'
  manual 47 'MAN/semantic/google_cloud_run_v2_job.migrate/open-settings-platform-architect' 'sha256:870c2e6f6690a6b8bb27b76a90c3d209eee27e53cf912778ef0fcf38755d7ba7' '' '' 'the review of the open settings of google_cloud_run_v2_job.migrate' 'execution/manual/actions.md'
  manual 48 'MAN/semantic/google_cloud_run_v2_job.migrate/secrets' 'sha256:ccf7ed77ed22045af7f02119e337a38136f98144a0499fc1811cd43b3be23943' '' '' 'the setting secrets of google_cloud_run_v2_job.migrate' 'execution/manual/actions.md'
  manual 49 'MAN/semantic/google_cloud_run_v2_job.migrate/service-identity' 'sha256:298d2eede307b1eb80e27d6b90d821cf9df516a6bab67a667e971af7c3a579b5' '' '' 'the setting service-identity of google_cloud_run_v2_job.migrate' 'execution/manual/actions.md'
  manual 50 'MAN/semantic/google_cloud_run_v2_service.server/cloud-sql-connection' 'sha256:e1e97ebcacd3826e89ba663ff7c92dac49e6625c57496f462611554469e2ff50' '' '' 'the setting cloud-sql-connection of google_cloud_run_v2_service.server' 'execution/manual/actions.md'
  manual 51 'MAN/semantic/google_cloud_run_v2_service.server/open-settings-identity-architect' 'sha256:4149a3fb8301178ae29c73bb323a46192f53483c9e863b5720cf385d633b9056' '' '' 'the review of the open settings of google_cloud_run_v2_service.server by the Identity Architect' 'execution/manual/actions.md'
  manual 52 'MAN/semantic/google_cloud_run_v2_service.server/open-settings-platform-architect' 'sha256:fdd1e6180bc5c50bfc2a7e495cbf14c355933fb3a265740049be24c6f241d845' '' '' 'the review of the open settings of google_cloud_run_v2_service.server by the Platform Architect' 'execution/manual/actions.md'
  manual 53 'MAN/semantic/google_cloud_run_v2_service.server/open-settings-security-architect' 'sha256:3b939d25926b91abf8b2cfefc1802f91870aff5318834c15e03dcc6bee6e7614' '' '' 'the review of the open settings of google_cloud_run_v2_service.server by the Security Architect' 'execution/manual/actions.md'
  manual 54 'MAN/semantic/google_pubsub_topic.faux/tc-name' 'sha256:81df48ba7364e2bc64f5aaa9e9f655353f62fce8a1f4906b80b1a0645271ebf8' '' '' 'the setting "name" of google_pubsub_topic.faux' 'execution/manual/actions.md'
  refuse 55 'REF/unit/google_firebase_project.default' 'sha256:dffdcefa92c9c47099446ad4827124ff99b1eab647368f5a55ddaefa6b692d10' '' '' 'the refusal of google_firebase_project.default' 'execution/manual/actions.md'
  manual 58 'MAN/draft/handover/cluster-access/kubeconfig-md' 'sha256:4a8a48e5fe7e137e8381f08d2774a3fbfce2453a794d0ddada1bbed73e2123cc' '' '' 'the step "Fetch the kubeconfig, check the health, create the namespace and the secrets of…"' 'terraform/handover/cluster-access/kubeconfig.md'
  phase 'WORKLOAD'
  manual 59 'MAN/draft/kubernetes/migrate/manifests-yaml' 'sha256:54f0e4a3b4da9c017f133b52b5e60c701a7ec808d9d8e5f0530dc3e5e4cb0124' '' '' 'the step "Fill in the required inputs of kubernetes/migrate/manifests.yaml and apply it…"' 'terraform/kubernetes/migrate/manifests.yaml'
  manual 60 'MAN/draft/kubernetes/server/manifests-yaml' 'sha256:6c9a6187d8852ec1403fd37fa067c2458c57915384809b591ced1edb674c0e1f' '' '' 'the step "Fill in the required inputs of kubernetes/server/manifests.yaml and apply it to…"' 'terraform/kubernetes/server/manifests.yaml'
  manual 61 'MAN/unit/google_cloudbuild_trigger.activategcb' 'sha256:4fce9e58e6122f1952d88bf38aea1fb92780391f4b25df15e80a5d84d81ab3b8' '' 'it waits for the decision DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.activategcb' 'the proposed alternative of google_cloudbuild_trigger.activategcb' 'execution/manual/actions.md'
  manual 62 'MAN/unit/google_cloudbuild_trigger.init' 'sha256:9470c78c9a0d60d234b1011b246c639b6b30fb03389db7169221a9ce389cc685' '' 'it waits for the decision DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.init' 'the proposed alternative of google_cloudbuild_trigger.init' 'execution/manual/actions.md'
  manual 63 'MAN/unit/google_cloudbuild_trigger.placeholder' 'sha256:a20e27c0b93f2e32005e3391deb1cf3c67c9ca99c44d053fe2d26aa65e6aad88' '' 'it waits for the decision DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.placeholder' 'the proposed alternative of google_cloudbuild_trigger.placeholder' 'execution/manual/actions.md'
  phase 'CUTOVER'
  manual 64 'MAN/data/google_sql_database_instance.postgres/write-freeze/dump-restore' 'sha256:172c3ec084557cfcadcfd5015ca95638ddfe51f16b1a8e1412b291a076341877' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the write freeze of google_sql_database_instance.postgres by dump and restore' 'execution/manual/actions.md'
  manual 65 'MAN/data/google_sql_database_instance.postgres/final-sync/dump-restore' 'sha256:675b52be810bb1bdd416b68cd6b25fc112e6ddc9900d37eca7794b3ec2638cb4' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the final copy of the data of google_sql_database_instance.postgres by dump and restore' 'execution/manual/actions.md'
  manual 66 'MAN/data/google_sql_database_instance.postgres/reconcile/dump-restore' 'sha256:161e1d17a24ef7d46cac948e5acd17f34a19cef4524f18eae433d0e354ece464' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the data reconciliation of google_sql_database_instance.postgres by dump and restore' 'execution/manual/actions.md'
  manual 67 'MAN/data/google_sql_database_instance.postgres/write-freeze/logical-replication' 'sha256:69ad6bc80d25e66f9aa44d166e7b20a0bfe5e2311c8e218a090444558c3d55f1' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the write freeze of google_sql_database_instance.postgres by logical replication' 'execution/manual/actions.md'
  manual 68 'MAN/data/google_sql_database_instance.postgres/final-sync/logical-replication' 'sha256:3cc4fc62254b8b2311f93f20e3d40c21ea80bd787ed1be06a37565163e7371d4' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the final copy of the data of google_sql_database_instance.postgres by logical replication' 'execution/manual/actions.md'
  manual 69 'MAN/data/google_sql_database_instance.postgres/reconcile/logical-replication' 'sha256:045ec09cbc04f8e06cb8150290715eae89d2f7c2f088cacad1aa8ba8437d3e80' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the data reconciliation of google_sql_database_instance.postgres by logical replication' 'execution/manual/actions.md'
  manual 70 'MAN/data/google_sql_database_instance.postgres/write-freeze/own-tool' 'sha256:5369abf396c411e9b5ce6fc6c3faba88e30e3d273acea9d673a48f6f8f90d2d9' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the write freeze of google_sql_database_instance.postgres with a tool of your own' 'execution/manual/actions.md'
  manual 71 'MAN/data/google_sql_database_instance.postgres/final-sync/own-tool' 'sha256:263dabcb04771bf85da682f8c8ae79e489b6b42e580990d5095ead050bbc86bc' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the final copy of the data of google_sql_database_instance.postgres with a tool of your own' 'execution/manual/actions.md'
  manual 72 'MAN/data/google_sql_database_instance.postgres/reconcile/own-tool' 'sha256:efd2f41cddffda58697cfe54ddc87d875c9a27024165563305ba91af70b68342' 'cutover' 'it waits for the decision DEC-GEN-CONVERT-DATA-MOVEMENT' 'the data reconciliation of google_sql_database_instance.postgres with a tool of your own' 'execution/manual/actions.md'
  manual 73 'MAN/data/google_storage_bucket.media/write-freeze/rclone-copy' 'sha256:a9ac77b4d1805a9c125eb187d12467e6c71c13ffbba9f06f10a9a97d2e32ae97' 'cutover' '' 'the write freeze of google_storage_bucket.media with rclone' 'execution/manual/actions.md'
  manual 74 'MAN/data/google_storage_bucket.media/final-sync/rclone-copy' 'sha256:4c6ad7fc625814cd1626a0d510123a45a13e44c1cab39fed31f9de8f34f7441d' 'cutover' '' 'the final copy of the data of google_storage_bucket.media with rclone' 'execution/manual/actions.md'
  manual 75 'MAN/data/google_storage_bucket.media/reconcile/rclone-copy' 'sha256:969577bf72ade37ab461145237197c8d5c08060970495a30674d893e7ddd38c1' 'cutover' '' 'the data reconciliation of google_storage_bucket.media with rclone' 'execution/manual/actions.md'
  finish 3 0
}

if [ "$mode" = run ]; then
  ready_run
  say "StackToIT deployment: a run, up to the first step that holds."
else
  say "StackToIT deployment: a dry run. Nothing is called, applied or written."
fi
read_journal
same_environment
the_plan
printf '\n'
if [ "$mode" = run ]; then
  say "The flow is complete up to the verification: every step of it is recorded in the journal."
  exit 0
fi
say "$done_steps steps are recorded as done, $stops steps or stages hold, and $would_steps OpenTofu and API steps have nothing of their own in the way: a run reaches them once the stops before them are closed."
if [ -n "$first_stop" ]; then
  say "A run stops first here: $first_stop"
fi
printf '\n'
prerequisites
