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
Selectel заменил адрес сервера в ночь на 27.08 по нашей заявке: старый 188.246.224.93 фильтровался российскими операторами и не открывался ни с домашнего интернета, ни с мобильного (#3110). Новый адрес фильтрации не имеет — проверено с машины владельца после переключения DNS. Замена уронила сервер на 10 часов: адрес на порте сменился, а в ОС остался прежний (разбор и восстановление — #3119). Здесь только то, что осталось в репозитории. Единственное функциональное вхождение — дефолт HOST_IP в ops/selectel-ci-access.sh: скрипт открывает раннеру доступ на прод-хост, и с прежним значением он молча настроил бы доступ на адрес, которого у нас больше нет. Остальные семь — тексты комментариев в Caddyfile-секциях, compose, bootstrap-скриптах и раннбуке крона; они не исполняются, но именно по ним сверяются при переезде, и разошедшийся адрес в них дороже, чем кажется.
178 lines
14 KiB
Bash
Executable file
178 lines
14 KiB
Bash
Executable file
#!/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 и их базы готовы; можно восстанавливать дампы целевыми ролями."
|