fix(infra): bootstrap_glitchtip — \gexec instead of DO block (psql var trap) #219

Merged
lekss361 merged 1 commit from fix/glitchtip-bootstrap-gexec into main 2026-05-16 15:54:30 +00:00
Owner

Контекст

scripts/bootstrap_glitchtip.sh упал в проде (см. #204) с ошибкой:

ERROR:  syntax error at or near ":"
LINE 4: ...format('CREATE ROLE glitchtip LOGIN PASSWORD %L', :'gt_pass'...

Корень проблемы

В исходной версии скрипта пароль для CREATE ROLE передавался внутрь DO $$ ... $$ блока через psql-переменную :'gt_pass'.

Это не работает: psql client-side переменные подставляются только в SQL-стриме psql, не внутри dollar-quoted body. Содержимое DO $$ ... $$ уходит на сервер как литеральный текст, и Postgres не знает что такое :'gt_pass' → syntax error.

Фикс

Заменил DO $$ ... $$ на \gexec-паттерн — он строит SQL-команду client-side через quote_literal(:'gt_pass'), потом \gexec отправляет результат на сервер.

SELECT 'CREATE ROLE glitchtip LOGIN PASSWORD ' || quote_literal(:'gt_pass') || ';'
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'glitchtip')
\gexec

Свойства:

  • Идемпотентно: WHERE NOT EXISTS фильтрует, \gexec получает 0 строк → no-op без ошибки.
  • No SQL-injection: quote_literal server-side эскейпит апострофы и бэкслеши.
  • ON_ERROR_STOP=1 гарантирует что ошибка в самом \gexec-команде уронит psql и скрипт.

Дополнительные фиксы

  1. -d postgres вместо -d "$POSTGRES_DB"CREATE DATABASE требует подключения к другой БД (нельзя создавать БД, будучи к ней подключенным). Раньше скрипт пытался создать glitchtip сидя в gendesign — могло работать случайно если приложение \c-ало, но это хрупко.
  2. GRANT ALL PRIVILEGES ON DATABASE glitchtip TO glitchtip — отсутствовало в оригинале. Без этого пользователь glitchtip не имел прав на свою же БД (owner-права отдельны от GRANT).

Test plan

  • bash -n scripts/bootstrap_glitchtip.sh — syntax OK
  • Idempotency: запустить дважды на чистой VPS — первый создаст, второй no-op
  • Pre-existing role/DB scenario: создать роль вручную, потом запустить bootstrap — должен пройти без ошибок
  • #204 — основной issue
  • #205 — PR где этот баг был внедрён
## Контекст `scripts/bootstrap_glitchtip.sh` упал в проде (см. #204) с ошибкой: ``` ERROR: syntax error at or near ":" LINE 4: ...format('CREATE ROLE glitchtip LOGIN PASSWORD %L', :'gt_pass'... ``` ## Корень проблемы В исходной версии скрипта пароль для CREATE ROLE передавался внутрь `DO $$ ... $$` блока через psql-переменную `:'gt_pass'`. **Это не работает**: psql client-side переменные подставляются только в SQL-стриме psql, **не внутри dollar-quoted body**. Содержимое `DO $$ ... $$` уходит на сервер как литеральный текст, и Postgres не знает что такое `:'gt_pass'` → syntax error. ## Фикс Заменил `DO $$ ... $$` на `\gexec`-паттерн — он строит SQL-команду client-side через `quote_literal(:'gt_pass')`, потом `\gexec` отправляет результат на сервер. ```sql SELECT 'CREATE ROLE glitchtip LOGIN PASSWORD ' || quote_literal(:'gt_pass') || ';' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'glitchtip') \gexec ``` Свойства: - **Идемпотентно**: `WHERE NOT EXISTS` фильтрует, `\gexec` получает 0 строк → no-op без ошибки. - **No SQL-injection**: `quote_literal` server-side эскейпит апострофы и бэкслеши. - **`ON_ERROR_STOP=1`** гарантирует что ошибка в самом `\gexec`-команде уронит psql и скрипт. ## Дополнительные фиксы 1. **`-d postgres` вместо `-d "$POSTGRES_DB"`** — `CREATE DATABASE` требует подключения к **другой** БД (нельзя создавать БД, будучи к ней подключенным). Раньше скрипт пытался создать `glitchtip` сидя в gendesign — могло работать случайно если приложение `\c`-ало, но это хрупко. 2. **`GRANT ALL PRIVILEGES ON DATABASE glitchtip TO glitchtip`** — отсутствовало в оригинале. Без этого пользователь `glitchtip` не имел прав на свою же БД (owner-права отдельны от GRANT). ## Test plan - [x] `bash -n scripts/bootstrap_glitchtip.sh` — syntax OK - [ ] Idempotency: запустить дважды на чистой VPS — первый создаст, второй no-op - [ ] Pre-existing role/DB scenario: создать роль вручную, потом запустить bootstrap — должен пройти без ошибок ## Related - #204 — основной issue - #205 — PR где этот баг был внедрён
lekss361 added 1 commit 2026-05-16 15:30:23 +00:00
psql variables (:'gt_pass') are substituted client-side in the psql stream
but NOT inside dollar-quoted DO $$..$$ bodies — those are sent to the server
as literal text, causing syntax error. Replace DO block with \gexec pattern:
quote_literal(:'gt_pass') builds the CREATE ROLE statement client-side, then
\gexec executes it. WHERE NOT EXISTS makes both role and DB steps idempotent.
Also connect to 'postgres' db (not app db) for CREATE DATABASE to work.
Author
Owner

Verdict: APPROVE

Root cause — корректно диагностирован

psql client-side вары :'name' НЕ раскрываются внутри dollar-quoted body ($$ ... $$) — Postgres получает текст as-is и не знает что такое :'gt_pass' → syntax error. Документация psql прямо это оговаривает.

Фикс — правильный

SELECT 'CREATE ROLE glitchtip LOGIN PASSWORD ' || quote_literal(:'gt_pass') || ';'
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'glitchtip')
\gexec
  • :'gt_pass' подставляется psql client-side до отправки на сервер — это валидная позиция (SQL-стрим psql, не dollar-quote).
  • quote_literal выполняется server-side в первом SELECT, эскейпит апострофы/бэкслеши — SQL-injection невозможна.
  • WHERE NOT EXISTS → 0 rows → \gexec no-op → идемпотентно без exception.
  • ON_ERROR_STOP=1 распространяется на команды, отправленные \gexec (psql exits non-zero).

Дополнительные фиксы — обоснованы

  1. -d postgres вместо -d "$POSTGRES_DB" — best practice для CREATE DATABASE (избегаем edge-case с активными соединениями к создаваемой БД).
  2. GRANT ALL PRIVILEGES ON DATABASE glitchtip TO glitchtip — формально redundant если роль = OWNER (owner имеет implicit privileges), но defensive и harmless + идемпотентно при повторном запуске.

Security

  • Пароль из $GLITCHTIP_DB_PASSquote_literal → нет injection-вектора.
  • psql invoke без --echo-queries / --echo-all → password не уходит в stderr/logs.
  • Heredoc <<'SQL' (single-quoted) — нет shell interpolation внутри SQL.

Nits (non-blocking)

  • GRANT ран unconditionally — нормально (GRANT идемпотентен), но можно было обернуть в тот же \gexec-pattern для consistency.

Test plan — отмечен в PR body

  • bash -n syntax check
  • Idempotency runs — рекомендую запустить дважды на prod после merge для confirmation.

Auto-merge: scripts/ scope (не в blocked-list) → squash merge.

<!-- gendesign-review-bot: sha=a1091cd verdict=approve --> ## Verdict: ✅ APPROVE ### Root cause — корректно диагностирован psql client-side вары `:'name'` НЕ раскрываются внутри dollar-quoted body (`$$ ... $$`) — Postgres получает текст as-is и не знает что такое `:'gt_pass'` → syntax error. Документация psql прямо это оговаривает. ### Фикс — правильный ```sql SELECT 'CREATE ROLE glitchtip LOGIN PASSWORD ' || quote_literal(:'gt_pass') || ';' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'glitchtip') \gexec ``` - `:'gt_pass'` подставляется psql client-side **до** отправки на сервер — это валидная позиция (SQL-стрим psql, не dollar-quote). - `quote_literal` выполняется server-side в первом SELECT, эскейпит апострофы/бэкслеши — **SQL-injection невозможна**. - `WHERE NOT EXISTS` → 0 rows → `\gexec` no-op → **идемпотентно** без exception. - `ON_ERROR_STOP=1` распространяется на команды, отправленные `\gexec` (psql exits non-zero). ### Дополнительные фиксы — обоснованы 1. **`-d postgres` вместо `-d "$POSTGRES_DB"`** — best practice для `CREATE DATABASE` (избегаем edge-case с активными соединениями к создаваемой БД). 2. **`GRANT ALL PRIVILEGES ON DATABASE glitchtip TO glitchtip`** — формально redundant если роль = OWNER (owner имеет implicit privileges), но defensive и **harmless** + идемпотентно при повторном запуске. ### Security - ✅ Пароль из `$GLITCHTIP_DB_PASS` → `quote_literal` → нет injection-вектора. - ✅ `psql` invoke без `--echo-queries` / `--echo-all` → password не уходит в stderr/logs. - ✅ Heredoc `<<'SQL'` (single-quoted) — нет shell interpolation внутри SQL. ### Nits (non-blocking) - `GRANT` ран unconditionally — нормально (GRANT идемпотентен), но можно было обернуть в тот же `\gexec`-pattern для consistency. ### Test plan — отмечен в PR body - [x] `bash -n` syntax check - [ ] Idempotency runs — рекомендую запустить дважды на prod после merge для confirmation. **Auto-merge:** scripts/ scope (не в blocked-list) → squash merge.
lekss361 merged commit 021b0589b2 into main 2026-05-16 15:54:30 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#219
No description provided.