gendesign/ops/db-bootstrap/infra-postgres/01-roles-and-databases.sh
bot-backend efaab2efda
All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (pull_request) Successful in 17m16s
chore(ops): адрес Poincare — 188.124.37.140 вместо заменённого 188.246.224.93 (#3110)
Selectel заменил адрес сервера в ночь на 27.08 по нашей заявке: старый
188.246.224.93 фильтровался российскими операторами и не открывался ни с
домашнего интернета, ни с мобильного (#3110). Новый адрес фильтрации не имеет —
проверено с машины владельца после переключения DNS.

Замена уронила сервер на 10 часов: адрес на порте сменился, а в ОС остался
прежний (разбор и восстановление — #3119). Здесь только то, что осталось в
репозитории.

Единственное функциональное вхождение — дефолт HOST_IP в ops/selectel-ci-access.sh:
скрипт открывает раннеру доступ на прод-хост, и с прежним значением он молча
настроил бы доступ на адрес, которого у нас больше нет. Остальные семь — тексты
комментариев в Caddyfile-секциях, compose, bootstrap-скриптах и раннбуке крона;
они не исполняются, но именно по ним сверяются при переезде, и разошедшийся
адрес в них дороже, чем кажется.
2026-08-27 11:23:37 +03:00

178 lines
14 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
# Роли и базы лёгкого инфраструктурного кластера `infra-postgres` (#3061).
#
# ЗАЧЕМ. Переезд продукта Beget (46.173.16.127) → Selectel Poincare
# (188.124.37.140), окно 30.08.2026. Контейнер `gendesign-postgres-1` уезжает
# целиком вместе с томом postgres_data (~15 ГБ), а внутри него сегодня живут
# четыре базы — и две из них должны ОСТАТЬСЯ на Beget:
# forgejo 265 МБ — git.gendsgn.ru + Actions CI (из него же идёт деплой)
# glitchtip 729 МБ — errors.gendsgn.ru
# Этот файл готовит им новый дом: роли-владельцы и пустые базы в кластере
# postgres:16-alpine (сервис `infra-postgres` в docker-compose.prod.yml).
#
# КОГДА ИСПОЛНЯЕТСЯ — ровно один раз. Каталог /docker-entrypoint-initdb.d
# официальный образ читает ТОЛЬКО на пустом PGDATA и ТОЛЬКО до того, как в
# кластер попадут какие-либо данные. Это та же мина, что описана у сервиса
# postgres в docker-compose.prod.yml (#2989) и в tradein-mvp: если том
# infra_postgres_data уже инициализирован, скрипты МОЛЧА не выполнятся — ни
# ошибки, ни строчки в логе. Чтобы прогнать заново: снести том
# (`docker volume rm gendesign_infra_postgres_data`) либо выполнить те же
# команды руками через `docker exec -i gendesign-infra-postgres psql`.
#
# ПОРЯДОК ОТНОСИТЕЛЬНО ДАМПОВ — ГЛАВНОЕ. Дампы снимаются с
# `pg_dump --no-owner --clean --if-exists` (см. ops/backup-forgejo.sh):
# --no-owner → в дампе нет ни одной `ALTER ... OWNER TO`, владельцем
# восстановленных объектов становится РОЛЬ, ОТ ИМЕНИ
# КОТОРОЙ ИДЁТ ВОССТАНОВЛЕНИЕ;
# --clean --if-exists → дамп начинается с DROP'ов, поэтому его одинаково
# можно лить и в пустую базу, и поверх существующей.
# Отсюда два жёстких следствия для окна миграции:
# 1. роли forgejo/glitchtip обязаны существовать ДО восстановления — иначе
# psql упадёт на первом же `GRANT ... TO forgejo`. Их создаёт этот файл;
# 2. лить дамп нужно ИМЕННО целевой ролью, а не суперюзером кластера:
# docker exec -i gendesign-infra-postgres \
# psql -v ON_ERROR_STOP=1 -U forgejo -d forgejo < forgejo.sql
# Восстановление суперюзером отдало бы ему все таблицы, и приложение
# получило бы "permission denied for table" на первой же записи.
# Если дамп содержит `CREATE EXTENSION` для НЕдоверенного расширения, владелец
# базы поставить его не сможет ("permission denied to create extension") —
# доверенные (pg_trgm, citext, btree_gin) в PG16 владельцу разрешены. Лечится
# разово: создать расширение суперюзером и повторить прогон дампа.
#
# ПАРОЛИ приходят из окружения контейнера (FORGEJO_DB_PASS / GLITCHTIP_DB_PASS
# в docker-compose.prod.yml ← /opt/gendesign/.env, chmod 600, вне git). В самом
# файле их нет и быть не может — только имена переменных. Не задан любой из
# двух → падаем громко: молча созданная беспарольная роль означала бы, что
# приложение не сможет залогиниться, и выяснилось бы это уже в окно миграции.
#
# ⚠️ У этого «падаем громко» есть цена, и её надо понимать. Порядок шагов
# энтрипойнта: PGDATA создаётся РАНЬШЕ, чем исполняется этот каталог. Значит наш
# exit 1 оставляет том ИНИЦИАЛИЗИРОВАННЫМ, но БЕЗ ролей и баз, а на следующем
# старте образ увидит непустой PG_VERSION и пропустит initdb.d НАВСЕГДА. Сам
# скрипт от этого защититься не может — он исполняется слишком поздно, — поэтому
# страховка вынесена в healthcheck сервиса infra-postgres: он требует наличия
# ОБЕИХ баз, и такой полупустой кластер никогда не станет healthy. Видно в
# `docker ps` в тот же день, а не в ночь переезда на заливке дампа. Лечение — то
# же, что описано выше: пересоздать том и поднять кластер заново с заполненными
# паролями.
#
# ⚠️ Файл ОБЯЗАН быть исполняемым (git mode 100755). Неисполняемые *.sh
# docker-entrypoint.sh не запускает, а ПОДКЛЮЧАЕТ через `.` — тогда `exit 1` из
# проверки паролей роняет сам энтрипойнт с невнятным кодом, а `set -u` протекает
# в его остаток. На Windows core.filemode=false, поэтому бит выставляется явно:
# `git update-index --chmod=+x ops/db-bootstrap/infra-postgres/01-roles-and-databases.sh`.
#
# bash в postgres:16-alpine есть — на нём написан сам docker-entrypoint.sh
# образа, так что shebang безопасен и без установки пакетов.
set -euo pipefail
fail() {
echo "infra-postgres bootstrap: ОШИБКА: $*" >&2
exit 1
}
[[ -n "${FORGEJO_DB_PASS:-}" ]] || fail \
"FORGEJO_DB_PASS пуст. Задать в /opt/gendesign/.env ДО первого старта кластера — потом initdb.d уже не выполнится."
[[ -n "${GLITCHTIP_DB_PASS:-}" ]] || fail \
"GLITCHTIP_DB_PASS пуст. Это тот же пароль, что стоит в DATABASE_URL у glitchtip-web/worker — он обязан совпасть, иначе после cutover GlitchTip не залогинится."
# Подключаемся к служебной БД `postgres`, а не к ${POSTGRES_DB}: CREATE DATABASE
# нельзя выполнить, находясь в создаваемой базе, и лишняя привязка к имени
# служебной базы кластера тут ни к чему (подробный разбор — в
# ops/db-bootstrap/create_auth_db.sql).
psql -v ON_ERROR_STOP=1 \
--username "$POSTGRES_USER" \
--dbname postgres \
-v forgejo_pw="$FORGEJO_DB_PASS" \
-v glitchtip_pw="$GLITCHTIP_DB_PASS" <<'EOSQL'
-- Пароли кладём в сессионные GUC, а не подставляем прямо в текст DO-блока:
-- psql НЕ интерполирует :'var' внутри dollar-quoted блока ($$...$$) — это
-- правило psql, а не баг (инцидент деплоя 2026-05-24, разобран в
-- ops/db-bootstrap/set_tradein_fdw_password.sql). set_config вызывается ВНЕ $$,
-- значит подстановка срабатывает, а внутрь блока значение приходит через
-- current_setting(); format(%L) экранирует его как SQL-литерал, поэтому пароль
-- с кавычками безопасен.
--
-- \o /dev/null вокруг set_config: функция ВОЗВРАЩАЕТ установленное значение —
-- без глушения psql напечатал бы пароль в stdout, то есть в лог контейнера
-- (`docker logs gendesign-infra-postgres` и journald, #2761).
\o /dev/null
SELECT set_config('app.forgejo_pw', :'forgejo_pw', false);
SELECT set_config('app.glitchtip_pw', :'glitchtip_pw', false);
\o
-- Роли. Идемпотентно: на пустом кластере это CREATE, при ручном повторном
-- прогоне — ALTER, синхронизирующий пароль с текущим окружением (важно, если
-- пароль в /opt/gendesign/.env поменяли, а том пересоздавать не хочется).
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'forgejo') THEN
EXECUTE format('CREATE ROLE forgejo LOGIN PASSWORD %L', current_setting('app.forgejo_pw'));
RAISE NOTICE 'роль forgejo создана';
ELSE
EXECUTE format('ALTER ROLE forgejo WITH LOGIN PASSWORD %L', current_setting('app.forgejo_pw'));
RAISE NOTICE 'роль forgejo уже существовала — пароль синхронизирован с окружением';
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'glitchtip') THEN
EXECUTE format('CREATE ROLE glitchtip LOGIN PASSWORD %L', current_setting('app.glitchtip_pw'));
RAISE NOTICE 'роль glitchtip создана';
ELSE
EXECUTE format('ALTER ROLE glitchtip WITH LOGIN PASSWORD %L', current_setting('app.glitchtip_pw'));
RAISE NOTICE 'роль glitchtip уже существовала — пароль синхронизирован с окружением';
END IF;
END $$;
-- Базы. CREATE DATABASE нельзя ни внутри DO-блока (это функция, она идёт в
-- транзакции), ни вообще в транзакционном блоке — поэтому идемпотентность
-- делается через \gexec: команда собирается на стороне клиента, и если
-- WHERE NOT EXISTS отфильтровал строку, \gexec не получает ничего и молча
-- ничего не делает. ON_ERROR_STOP=1 распространяется и на \gexec.
--
-- TEMPLATE template0 — сознательно, а не template1: template0 гарантированно
-- пуст и неизменяем, тогда как в template1 кто угодно мог доустановить объекты
-- или расширения, и они молча оказались бы внутри баз Forgejo/GlitchTip.
-- ENCODING 'UTF8' задан явно, чтобы кириллица в issue и комментариях Forgejo и
-- в заголовках событий GlitchTip не зависела от того, с какими аргументами
-- когда-нибудь пересоздадут кластер.
--
-- OWNER — прикладная роль, а НЕ суперюзер (в отличие от базы `auth`, где
-- владельцем сознательно оставлен суперюзер): дампы сняты с --no-owner и
-- восстанавливаются от имени самой роли, значит она обязана иметь право
-- создавать объекты в своей базе.
SELECT 'CREATE DATABASE forgejo OWNER forgejo TEMPLATE template0 ENCODING ''UTF8'';'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'forgejo')
\gexec
SELECT 'CREATE DATABASE glitchtip OWNER glitchtip TEMPLATE template0 ENCODING ''UTF8'';'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'glitchtip')
\gexec
-- По умолчанию PostgreSQL выдаёт CONNECT на новую БД роли PUBLIC — то есть
-- glitchtip мог бы открыть сессию в базе Forgejo и наоборот. В общем кластере
-- на публично доступном хосте это лишнее. Владельцу REVOKE ничего не отнимает:
-- права владельца объекта не берутся из ACL. Команды идемпотентны.
REVOKE ALL ON DATABASE forgejo FROM PUBLIC;
REVOKE ALL ON DATABASE glitchtip FROM PUBLIC;
COMMENT ON DATABASE forgejo IS
'Forgejo (git.gendsgn.ru + Actions CI). Перенесена из gendesign-postgres-1 при '
'переезде продукта на Selectel (#3061). Бэкап — ops/backup-forgejo.sh.';
COMMENT ON DATABASE glitchtip IS
'GlitchTip (errors.gendsgn.ru). Перенесена из gendesign-postgres-1 при переезде '
'продукта на Selectel (#3061).';
-- Чистим GUC после использования — defense-in-depth, чтобы пароль не оставался
-- в состоянии сессии даже на короткое время. Тот же приём с \o: set_config
-- возвращает пустую строку, но лишний ряд в stdout не нужен.
\o /dev/null
SELECT set_config('app.forgejo_pw', '', false);
SELECT set_config('app.glitchtip_pw', '', false);
\o
EOSQL
echo "infra-postgres bootstrap: роли forgejo/glitchtip и их базы готовы; можно восстанавливать дампы целевыми ролями."