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