#!/usr/bin/env bash # Restore a Postgres dump produced by backup.sh. # # Usage: # ops/restore.sh /opt/gendesign/backups/gendesign_20260427_033000.sql.gz # # Refuses to run unless RESTORE_CONFIRM=yes is set. This is destructive — # the dump uses --clean --if-exists, so it DROPs existing tables before recreating. # # Example: # RESTORE_CONFIRM=yes ops/restore.sh ./gendesign_20260427_033000.sql.gz # # GLOBALS (#3057). Дамп из backup.sh не содержит ролей: pg_dump выгружает одну # базу, а роли живут в кластере и уезжают отдельным файлом # gendesign_globals_.sql.gz, который backup.sh кладёт РЯДОМ. При этом дамп # ссылается на роли в OWNER TO и GRANT, а psql здесь идёт с ON_ERROR_STOP=1 — # значит на кластере, где роли ещё не заведены, восстановление падает на первом # же таком операторе. Данные к тому моменту уже залиты и закоммичены (psql не # оборачивает дамп в одну транзакцию), то есть работа уходит в наполовину # собранную базу с невнятной ошибкой в самом хвосте. # # Поэтому globals-файл рядом с дампом подхватывается АВТОМАТИЧЕСКИ и грузится # ПЕРВЫМ — ровно как это давно делает ops/restore-drill.sh. # # ПОБОЧНЫЙ ЭФФЕКТ, о котором надо знать: globals несёт пароли ролей на момент # снятия дампа. Если пароль меняли после — он вернётся к старому значению. # Для поднятия кластера с нуля это правильно, для точечного отката данных в # живой базе может быть неожиданно, поэтому есть два рычага: # RESTORE_SKIP_GLOBALS=1 — не трогать роли вообще # RESTORE_GLOBALS_FILE=... — взять конкретный файл, а не соседний # # Если globals не нашёлся и не отключён явно — предупреждаем ДО заливки, чтобы # «упало через час» не стало сюрпризом. set -euo pipefail if [[ "${RESTORE_CONFIRM:-no}" != "yes" ]]; then echo "Refusing to restore — set RESTORE_CONFIRM=yes to proceed." echo "This will DROP existing tables and recreate from the dump." exit 1 fi DUMP_FILE="${1:-}" if [[ -z "$DUMP_FILE" || ! -f "$DUMP_FILE" ]]; then echo "Usage: $0 /path/to/dump.sql.gz" exit 2 fi COMPOSE_DIR=/opt/gendesign COMPOSE_FILE=docker-compose.prod.yml cd "$COMPOSE_DIR" DB_USER=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_USER 2>/dev/null | tr -d '\r' || true) DB_NAME=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_DB 2>/dev/null | tr -d '\r' || true) DB_USER=${DB_USER:-gendesign} DB_NAME=${DB_NAME:-gendesign} # --- globals: роли кластера, без которых упадут OWNER TO / GRANT --------------- # Имя соседа строится по той же схеме, что использует backup.sh: # gendesign_.sql.gz -> gendesign_globals_.sql.gz GLOBALS_FILE="${RESTORE_GLOBALS_FILE:-}" if [[ "${RESTORE_SKIP_GLOBALS:-0}" != "1" && -z "$GLOBALS_FILE" ]]; then dump_dir=$(cd "$(dirname "$DUMP_FILE")" && pwd) dump_base=$(basename "$DUMP_FILE") if [[ "$dump_base" =~ ^([A-Za-z0-9]+)_([0-9]{8}_[0-9]{6})\.sql\.gz$ ]]; then candidate="${dump_dir}/${BASH_REMATCH[1]}_globals_${BASH_REMATCH[2]}.sql.gz" [[ -f "$candidate" ]] && GLOBALS_FILE="$candidate" fi fi if [[ "${RESTORE_SKIP_GLOBALS:-0}" == "1" ]]; then echo "Globals: пропущены явно (RESTORE_SKIP_GLOBALS=1)." elif [[ -n "$GLOBALS_FILE" ]]; then if [[ ! -f "$GLOBALS_FILE" ]]; then echo "ОШИБКА: globals-файл не найден: $GLOBALS_FILE" >&2 exit 2 fi echo "Globals: $GLOBALS_FILE -> кластер (роли и их пароли на момент дампа)" # ON_ERROR_STOP здесь НЕ ставим: на живом кластере роли уже есть, и # CREATE ROLE для каждой из них законно падает с «already exists». # Значимая часть — идущие следом ALTER ROLE, они отрабатывают. gunzip -c "$GLOBALS_FILE" | \ docker compose -f "$COMPOSE_FILE" exec -T postgres \ psql -U "$DB_USER" -d postgres else echo "############################################################" >&2 echo "# ВНИМАНИЕ: globals-файл рядом с дампом не найден. #" >&2 echo "# Дамп ссылается на роли кластера в OWNER TO и GRANT. #" >&2 echo "# Если роли на этом кластере не заведены, psql упадёт по #" >&2 echo "# ON_ERROR_STOP уже ПОСЛЕ заливки данных. #" >&2 echo "# Ожидаемое имя: <проект>_globals_<та же метка>.sql.gz #" >&2 echo "############################################################" >&2 fi echo "Restoring $DUMP_FILE → ${DB_NAME} as ${DB_USER}" gunzip -c "$DUMP_FILE" | \ docker compose -f "$COMPOSE_FILE" exec -T postgres \ psql -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 echo "Restore complete."