Files
geointel/scripts/restore_release_backup_smoke.sh
T
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

134 lines
4.6 KiB
Bash

#!/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" <<PY
import json
import pathlib
payload = {
"schema_version": 1,
"verified_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"backup_dir": ${BACKUP_DIR@Q},
"temporary_database": ${TARGET_DB@Q},
"production_database_untouched": True,
"temporary_database_retained": ${KEEP_DATABASE@Q} == "true",
"alembic_head": ${ALEMBIC_HEAD@Q},
"postgis_version": ${POSTGIS_VERSION@Q},
"table_counts_match": True,
}
path = pathlib.Path(__import__("sys").argv[1]).expanduser().resolve()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
PY
echo "Isolated restore verified: $TARGET_DB"
if [ "$KEEP_DATABASE" != "true" ]; then
cleanup_database
trap - EXIT
echo "Temporary verification database removed."
else
trap - EXIT
echo "Temporary verification database retained by explicit request: $TARGET_DB"
fi