fix(ops): восстановление не падает в хвосте из-за незаведённых ролей
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

Дамп 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
This commit is contained in:
bot-backend 2026-08-25 08:18:16 +03:00
parent de157b1024
commit 8a5d323608

View file

@ -9,6 +9,28 @@
#
# 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
@ -34,6 +56,43 @@ DB_NAME=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_D
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" | \