gendesign/ops/restore.sh
bot-backend 8a5d323608
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
fix(ops): восстановление не падает в хвосте из-за незаведённых ролей
Дамп backup.sh — это одна база, а роли живут в кластере и уезжают соседним
файлом gendesign_globals_<ts>.sql.gz. Сам дамп на эти роли ссылается: OWNER TO
и GRANT. psql здесь идёт с ON_ERROR_STOP=1, поэтому на кластере, где ролей ещё
нет, восстановление обрывается на первом же таком операторе — но данные к тому
моменту уже залиты и закоммичены, в одну транзакцию psql дамп не оборачивает.
Итог: наполовину собранная база и невнятная ошибка в самом конце, после всей
работы.

Ровно этот путь и нужен при переезде: на новом хосте кластер пустой, ролей нет.

Теперь соседний globals подхватывается автоматически и грузится первым — так
уже давно устроен ops/restore-drill.sh, разошлись только эти два скрипта.
Правило имени взято оттуда же.

Побочный эффект назван в шапке прямо: globals несёт пароли ролей на момент
снятия дампа, и если пароль меняли после, он вернётся к старому. Для поднятия
кластера с нуля это правильное поведение, для точечного отката данных в живой
базе — нет, поэтому есть RESTORE_SKIP_GLOBALS=1 и RESTORE_GLOBALS_FILE.

Сам globals грузится БЕЗ ON_ERROR_STOP: на живом кластере роли уже есть и
CREATE ROLE для каждой законно падает с already exists. Значимая часть — идущие
следом ALTER ROLE, они отрабатывают.

Если соседа нет и он не отключён явно — печатаем предупреждение ДО заливки,
чтобы «упадёт через час» не стало сюрпризом. Не падаем: на живом кластере роли
на месте и всё пройдёт как раньше.

Проверено: bash -n; ветка автоопределения прогнана на паре файлов с реальными
именами — сосед находится. Поведение на живом кластере не меняется: globals
идёт первым, дальше всё как было.

Refs #3057
2026-08-25 08:18:16 +03:00

102 lines
5.6 KiB
Bash
Executable file
Raw 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."