134 lines
4.6 KiB
Bash
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
|