gendesign/ops/restore.sh
lekss361 d99f733f41
All checks were successful
Deploy Infra Host / sync-infra-host (push) Successful in 4s
Deploy / build-backend (push) Has been skipped
Deploy / changes (push) Successful in 8s
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy / deploy (push) Successful in 1m46s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
fix(ops): восстановление не падает в хвосте из-за незаведённых ролей (#3089)
2026-08-25 05:18:53 +00:00

102 lines
5.6 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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_<ts>.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_<ts>.sql.gz -> gendesign_globals_<ts>.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."