#!/usr/bin/env bash set -euo pipefail CONTAINER="geointel" BACKUP_DIR="" OUTPUT="" CONFIRM="false" RESTORE_RESULT="" TARGET_DB="" usage() { cat <<'EOF' Usage: bash scripts/verify_release_upgrade_smoke.sh \ --backup-dir PATH --confirm-isolated-upgrade [options] Restores a verified release backup into a generated temporary database, runs the currently deployed image's Alembic upgrade against that database, verifies PostGIS and the single expected head, then removes the temporary database. Options: --container NAME --output PATH EOF } while [ "$#" -gt 0 ]; do case "$1" in --backup-dir) BACKUP_DIR="$2"; shift 2 ;; --container) CONTAINER="$2"; shift 2 ;; --output) OUTPUT="$2"; shift 2 ;; --confirm-isolated-upgrade) CONFIRM="true"; shift ;; --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-upgrade 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")" RESTORE_RESULT="$(mktemp "${TMPDIR:-/tmp}/geointel-upgrade-restore.XXXXXX.json")" cleanup() { if [ -n "$TARGET_DB" ] && [[ "$TARGET_DB" =~ ^geointel_restore_verify_[0-9_]+$ ]]; then db_user="$(docker exec "$CONTAINER" sh -c 'printf %s "${POSTGRES_USER:-${GEOINTEL_POSTGRES_USER:-geointel}}"' 2>/dev/null || true)" if [ -n "$db_user" ]; then docker exec "$CONTAINER" dropdb --if-exists -U "$db_user" "$TARGET_DB" >/dev/null 2>&1 || true fi fi rm -f -- "$RESTORE_RESULT" } trap cleanup EXIT bash "$(dirname "$0")/restore_release_backup_smoke.sh" \ --backup-dir "$BACKUP_DIR" \ --container "$CONTAINER" \ --confirm-isolated-restore \ --keep-database \ --output "$RESTORE_RESULT" TARGET_DB="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8"))["temporary_database"])' "$RESTORE_RESULT")" if ! [[ "$TARGET_DB" =~ ^geointel_restore_verify_[0-9_]+$ ]]; then echo "Unsafe temporary database returned by restore smoke: ${TARGET_DB}" >&2 exit 3 fi docker exec -e TARGET_DB="$TARGET_DB" "$CONTAINER" sh -c ' target_url="$(python -c '"'"' import os from sqlalchemy import URL print( URL.create( "postgresql+psycopg", username=os.environ["GEOINTEL_POSTGRES_USER"], password=os.environ["GEOINTEL_POSTGRES_PASSWORD"], host="127.0.0.1", port=5432, database=os.environ["TARGET_DB"], ) .render_as_string(hide_password=False) ) '"'"')" export DATABASE_URL="$target_url" python -m alembic upgrade head ' DB_USER="$(docker exec "$CONTAINER" sh -c 'printf %s "${POSTGRES_USER:-${GEOINTEL_POSTGRES_USER:-geointel}}"' )" 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="$(docker exec "$CONTAINER" sh -c 'python -m alembic heads | awk "{print \$1}"')" POSTGIS_VERSION="$(docker exec "$CONTAINER" psql -X -v ON_ERROR_STOP=1 -U "$DB_USER" -d "$TARGET_DB" -Atqc \ "SELECT postgis_version();")" RELEASE_REVISION="$(docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$CONTAINER")" if [ "$ALEMBIC_HEAD" != "$EXPECTED_HEAD" ]; then echo "Upgraded temporary database head '${ALEMBIC_HEAD}' differs from '${EXPECTED_HEAD}'." >&2 exit 4 fi if [ -z "$OUTPUT" ]; then OUTPUT="${BACKUP_DIR%/}-upgrade-smoke.json" fi python3 - "$OUTPUT" <