#!/usr/bin/env bash set -euo pipefail CONTAINER="geointel" BACKUP_DIR="" CONFIRM="false" KEEP_DATABASE="false" OUTPUT="" usage() { cat <<'EOF' Usage: bash scripts/restore_release_backup_smoke.sh --backup-dir PATH \ --confirm-isolated-restore [options] Restores into a generated geointel_restore_verify_* database only, verifies PostGIS, Alembic and retained table counts, then drops that temporary database. It refuses the production database name and never uses pg_restore --clean. Options: --container NAME --output PATH Write result JSON outside the immutable backup directory --keep-database Keep the isolated verification database for inspection EOF } while [ "$#" -gt 0 ]; do case "$1" in --backup-dir) BACKUP_DIR="$2"; shift 2 ;; --container) CONTAINER="$2"; shift 2 ;; --confirm-isolated-restore) CONFIRM="true"; shift ;; --keep-database) KEEP_DATABASE="true"; shift ;; --output) OUTPUT="$2"; shift 2 ;; --help|-h) usage; exit 0 ;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [ "$CONFIRM" != "true" ] || [ -z "$BACKUP_DIR" ]; then echo "Explicit --confirm-isolated-restore and --backup-dir are required." >&2 exit 2 fi for required in docker python3; do command -v "$required" >/dev/null 2>&1 || { echo "Missing required command: $required" >&2 exit 2 } done BACKUP_DIR="$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$BACKUP_DIR")" bash "$(dirname "$0")/verify_release_backup.sh" --backup-dir "$BACKUP_DIR" --container "$CONTAINER" DB_NAME="$(docker exec "$CONTAINER" sh -c 'printf %s "${POSTGRES_DB:-${GEOINTEL_POSTGRES_DB:-geointel}}"' )" DB_USER="$(docker exec "$CONTAINER" sh -c 'printf %s "${POSTGRES_USER:-${GEOINTEL_POSTGRES_USER:-geointel}}"' )" TARGET_DB="geointel_restore_verify_$(date -u +%Y%m%d%H%M%S)_$$" if [ "$TARGET_DB" = "$DB_NAME" ] || ! [[ "$TARGET_DB" =~ ^geointel_restore_verify_[0-9_]+$ ]]; then echo "Unsafe restore target: $TARGET_DB" >&2 exit 3 fi if docker exec "$CONTAINER" psql -X -U "$DB_USER" -d postgres -Atqc \ "SELECT 1 FROM pg_database WHERE datname = '$TARGET_DB';" | grep -q 1; then echo "Generated restore database already exists." >&2 exit 3 fi cleanup_database() { if [ "$KEEP_DATABASE" != "true" ]; then docker exec "$CONTAINER" dropdb --if-exists -U "$DB_USER" "$TARGET_DB" >/dev/null 2>&1 || true fi } trap cleanup_database EXIT docker exec "$CONTAINER" createdb -U "$DB_USER" "$TARGET_DB" docker exec -i "$CONTAINER" pg_restore \ --exit-on-error \ --no-owner \ --no-privileges \ -U "$DB_USER" \ -d "$TARGET_DB" < "$BACKUP_DIR/database.dump" POSTGIS_VERSION="$(docker exec "$CONTAINER" psql -X -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$TARGET_DB" -Atqc \ "SELECT postgis_version();")" ALEMBIC_HEAD="$(docker exec "$CONTAINER" psql -X -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$TARGET_DB" -Atqc \ "SELECT version_num FROM alembic_version;")" EXPECTED_HEAD="$(awk -F $'\t' '$1 == "alembic_head" { print $2 }' "$BACKUP_DIR/database-metadata.tsv")" if [ "$ALEMBIC_HEAD" != "$EXPECTED_HEAD" ]; then echo "Restored Alembic head '$ALEMBIC_HEAD' differs from '$EXPECTED_HEAD'." >&2 exit 4 fi while IFS=$'\t' read -r table expected; do [[ "$table" =~ ^[a-z_]+$ ]] || { echo "Unsafe table name in retained counts: $table" >&2 exit 4 } actual="$(docker exec "$CONTAINER" psql -X -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$TARGET_DB" -Atqc \ "SELECT count(*) FROM public.${table};")" if [ "$actual" != "$expected" ]; then echo "Restored count mismatch for $table: expected $expected, got $actual." >&2 exit 4 fi done < "$BACKUP_DIR/table-counts.tsv" if [ -z "$OUTPUT" ]; then OUTPUT="${BACKUP_DIR%/}-restore-smoke.json" fi python3 - "$OUTPUT" <