#!/usr/bin/env bash # The estate: what it is, what would change, and — behind a gate — changing it. # # Usage: # ./estate.sh # show # ./estate.sh list # every estate/*.json # ./estate.sh plan # tofu plan, read-only # ./estate.sh apply --yes # refuses without --yes # ./estate.sh destroy --yes # # Why the default is read-only: ../README.md set -euo pipefail cd "$(dirname "$0")" source ./lib/config.sh source ./lib/estate.sh load_config show() { echo "estate: $ESTATE ($ESTATE_FILE)" echo "target: $TARGET (cloud=$CLOUD region=$REGION)" echo "domain: $DOMAIN" echo "host: $HOST (sudo: $HOST_ADMIN)" echo "workspace: $TOFU_WORKSPACE" echo local status; status=$(estate_get "_meta.status") [ -n "$status" ] && echo " !! $status" && echo echo "services in scope for '$TARGET':" local name host up kind raw while IFS=$'\x1f' read -r name host up kind raw placement peer port lhost; do [ -z "$name" ] && continue printf ' %-12s %-14s %-24s %s%s\n' \ "$name" "${host:--}" "${up:--}" "$kind" \ "$([ -n "$raw" ] && echo ' [hand-written]')" done < <(estate_services "$TARGET") echo echo "cert SANs (derived, not listed):" estate_sans | sed 's/^/ /' } list() { local f n printf '%-12s %-16s %s\n' ESTATE DOMAIN STATUS for f in ../estate/*.json; do [ -f "$f" ] || continue n=$(basename "$f" .json) printf '%-12s %-16s %s%s\n' "$n" \ "$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("domain",""))' "$f")" \ "$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("_meta",{}).get("status",""))' "$f")" \ "$([ "$n" = "$ESTATE" ] && echo ' <- this one')" done } # Read-only. Meaningful only once state is imported: against empty state, # plan reports "create N resources", which is not drift. plan() { echo "== $TOFU_BIN plan ==" if ! command -v "$TOFU_BIN" >/dev/null; then echo " $TOFU_BIN not installed — skipped." >&2 echo " OpenTofu is the MPL-2.0 fork; 'terraform' works identically." >&2 else echo " would run: $TOFU_BIN plan -var-file=<(estate)" fi echo echo "NOTE: not wired up yet, and that is the point. tofu plan against empty" echo " state reports \"create N resources\" — which is not drift, it is an" echo " empty state. It becomes the check that proves the description" echo " matches reality only once state is IMPORTED from the inventory." echo " ppl/infra/ describes an aspiration: it was never applied." } # The gate. Two things have to be true: --yes present, AND the plan shown first. require_yes() { local verb="$1"; shift local yes="" for a in "$@"; do [ "$a" = "--yes" ] && yes=1; done if [ -z "$yes" ]; then echo "refusing to $verb without --yes." >&2 echo >&2 echo " $verb changes a live, billable estate and can take DNS with it." >&2 echo " Read the plan first: make estate plan" >&2 echo " Then: ./ctrl/estate.sh $verb --yes" >&2 exit 1 fi echo "refusing to $verb: not implemented, and deliberately so." >&2 echo " The executor is not wired up, and nothing is imported yet, so" >&2 echo " there is nothing truthful to apply." >&2 exit 1 } case "${1:-show}" in show) show ;; list) list ;; plan) plan ;; apply) shift; require_yes apply "$@" ;; destroy) shift; require_yes destroy "$@" ;; *) echo "usage: $0 [show|list|plan|apply --yes|destroy --yes]" >&2; exit 1 ;; esac