#!/usr/bin/env bash # Restore a tenant backup into a FRESH database, mirroring cmd/provision-tenant's prep, then verify. # DESTRUCTIVE to the target database. Before a SAME NAME restore, take the tenant offline first # (directory.SetStatus offline, or UPDATE tenant_directory SET status='offline'), because the router does # not filter by status and would route live requests into a database being recreated; mark it active again # only after a green verify. # # A tenant database has TWO schemas (public and the segregated eeo) and the migrator owns every table, so: # - the target is created from template0 with CREATE EXTENSION vector and the schema and connect grants # in place (the superuser only steps), exactly as provision-tenant does, BEFORE the restore; # - pg_restore runs as the MIGRATOR (never with --no-owner) so the tables come back migrator owned; # - verify compares per table row counts (public and eeo) and a vector sample, and re asserts the # isolation invariants. # Pin the client to PostgreSQL 16 (set PGBIN). Do NOT combine -j with --single-transaction. # # Usage: # scripts/restore-tenant.sh \ # -admin-dsn "host=/tmp user=postgres dbname=postgres sslmode=disable" \ # -migrator-dsn "host=H port=5432 user=missionmatch_migrator password=... dbname=postgres sslmode=disable" \ # -source tenant_acme -target tenant_acme_restored -dump FILE set -euo pipefail PGBIN="${PGBIN:-}" admin=""; mig=""; source=""; target=""; dump="" while [ $# -gt 0 ]; do case "$1" in -admin-dsn) admin="$2"; shift 2 ;; -migrator-dsn) mig="$2"; shift 2 ;; -source) source="$2"; shift 2 ;; -target) target="$2"; shift 2 ;; -dump) dump="$2"; shift 2 ;; *) echo "restore-tenant: unknown argument $1" >&2; exit 2 ;; esac done for v in admin mig source target dump; do eval "val=\$$v" [ -n "$val" ] || { echo "restore-tenant: -$v is required" >&2; exit 2; } done # CREATE DATABASE cannot be parameterized, so the name must be validated, never interpolated blindly. echo "$target" | grep -Eq '^[a-z][a-z0-9_]{2,62}$' || { echo "restore-tenant: target name $target is not a safe identifier" >&2; exit 2; } [ -f "$dump" ] || { echo "restore-tenant: dump file $dump not found" >&2; exit 2; } psql="${PGBIN:+$PGBIN/}psql" pgrestore="${PGBIN:+$PGBIN/}pg_restore" # Swap the database name in a keyword DSN (dbname=...). swapdb() { echo "$1" | sed -E "s/dbname=[a-zA-Z0-9_]+/dbname=$2/"; } admTarget="$(swapdb "$admin" "$target")" migTarget="$(swapdb "$mig" "$target")" migSource="$(swapdb "$mig" "$source")" echo "restore-tenant: 1/4 create the target database $target from template0 (superuser)" "$psql" "$admin" -v ON_ERROR_STOP=1 -c "CREATE DATABASE $target TEMPLATE template0" echo "restore-tenant: 2/4 prepare the target (extension, schema and connect grants), mirroring provision-tenant" "$psql" "$admTarget" -v ON_ERROR_STOP=1 <&2 exit 1 fi