fix(observability): привилегированную роль спрашиваем у контейнера, а не угадываем (#3078)
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 / changes (pull_request) Successful in 12s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m2s
CI / backend-tests (pull_request) Successful in 17m21s
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 / changes (pull_request) Successful in 12s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m2s
CI / backend-tests (pull_request) Successful in 17m21s
Стек наблюдаемости не поднялся ни на одном хосте после мержа #3099: джоба server упала, agent-apps и agent-infra пропустились как зависимые. Причина (задача 23657, 26.08 08:55): err: ОШИБКА: не нашёл роль с правом CREATE ROLE в gendesign-infra-postgres setup-metrics-grafana-role.sh искал привилегированную роль перебором трёх имён - glitchtip, forgejo, postgres. Ни одно не совпадает ни с одним реальным кластером проекта: infra-postgres -> infra, gendesign-postgres-1 -> gendesign, tradein-postgres -> tradein. Комментарий над перебором сам предупреждал, что "угадывать postgres неверно", и дальше шло угадывание. Замер на живом контейнере 26.08 (read-only, ничего не создавалось): POSTGRES_USER изнутри контейнера: infra glitchtip - отказ, forgejo - отказ, postgres - отказ, infra - 1 Стало: имя берём из POSTGRES_USER самого контейнера - это та переменная, которой роль и создана при initdb, то есть источник истины. Прежний список оставлен ПОСЛЕ него запасным путём для кластера не из образа postgres. Попутно - глоб в paths деплоя. Воркфлоу запускает ТРИ setup-скрипта, а в триггере стоял только setup-metrics-secrets.sh: правка двух остальных не заводила выкат, и на хосте молча оставалась старая версия. Тот же класс, что #2203 закрыл глобом ops/*.sh. Добавлен тест, который сверяет запускаемые скрипты с шаблонами paths - на исходном воркфлоу он краснеет, указывая на setup-metrics-exporter-dsn.sh. Тесты (6) исполняют РЕАЛЬНЫЙ скрипт с подставным docker и проверяют фактический выбор роли, а не наличие правильных слов в комментарии. Фальсификация: на исходном коде краснеют 3 из 5 ролевых тестов; проходят только те два, что фиксируют сохранённое поведение (запасной перебор и громкая ошибка при отсутствии привилегий). tests/ops целиком - 28 passed.
This commit is contained in:
parent
ad6fc1b2d6
commit
33bc7e4acf
3 changed files with 204 additions and 2 deletions
|
|
@ -22,7 +22,11 @@ on:
|
|||
- "caddy/sites/infra.caddy"
|
||||
- "caddy/metrics-ui.caddy.snippet"
|
||||
- "caddy/metrics-ingest.caddy.snippet"
|
||||
- "scripts/setup-metrics-secrets.sh"
|
||||
# Глоб, а не точечный `setup-metrics-secrets.sh` (#2203: класс бага, а не
|
||||
# один файл). Деплой запускает ТРИ setup-скрипта — secrets, grafana-role и
|
||||
# exporter-dsn, — а в триггере стоял только первый: правка двух остальных
|
||||
# не заводила выкат, и на хосте продолжала исполняться старая версия молча.
|
||||
- "scripts/setup-metrics-*.sh"
|
||||
- ".forgejo/workflows/deploy-metrics.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
|
|||
181
backend/tests/ops/test_3078_metrics_role_discovery.py
Normal file
181
backend/tests/ops/test_3078_metrics_role_discovery.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
"""Regression: привилегированная роль ищется у контейнера, а не угадывается (#3078).
|
||||
|
||||
Что произошло. `scripts/setup-metrics-grafana-role.sh` заводит read-only роль
|
||||
для датасорса Grafana и для этого ищет роль с правом CREATE ROLE. Искал он её
|
||||
перебором трёх имён:
|
||||
|
||||
for candidate in glitchtip forgejo postgres; do
|
||||
|
||||
Ни одно из трёх не совпадает ни с одним реальным кластером проекта — имя роли
|
||||
задаётся переменной `POSTGRES_USER` образа postgres и у нас везде своё:
|
||||
|
||||
gendesign-infra-postgres → infra
|
||||
gendesign-postgres-1 → gendesign
|
||||
tradein-postgres → tradein
|
||||
|
||||
Поэтому деплой стека наблюдаемости падал на первом же прогоне после мержа #3099
|
||||
(задача 23657, 26.08 08:55):
|
||||
|
||||
err: ОШИБКА: не нашёл роль с правом CREATE ROLE в gendesign-infra-postgres
|
||||
|
||||
и вместе с ним пропускались зависимые джобы `agent-apps` / `agent-infra` — ни
|
||||
одного контейнера стека не поднялось ни на одном хосте.
|
||||
|
||||
Замер на живом контейнере 26.08 (read-only, ничего не создавалось):
|
||||
|
||||
POSTGRES_USER изнутри контейнера: infra
|
||||
кандидат glitchtip → отказ
|
||||
кандидат forgejo → отказ
|
||||
кандидат postgres → отказ
|
||||
кандидат infra → 1
|
||||
|
||||
Фикс — спросить контейнер вместо угадывания: `POSTGRES_USER` это ровно та
|
||||
переменная, которой роль создана при initdb, то есть источник истины. Прежний
|
||||
список оставлен ПОСЛЕ него запасным путём — он пригодится кластеру, поднятому
|
||||
не из образа postgres, где переменная пуста.
|
||||
|
||||
ИРОНИЯ, РАДИ КОТОРОЙ ЭТОТ ТЕСТ: в комментарии над самим перебором было написано
|
||||
«угадывать «postgres» неверно» — и дальше шло угадывание. Тест ниже исполняет
|
||||
РЕАЛЬНЫЙ скрипт с подставным `docker` и проверяет фактический выбор роли, а не
|
||||
наличие правильных слов в комментарии.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# backend/tests/ops/<этот файл> → корень репозитория
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPT = REPO_ROOT / "scripts" / "setup-metrics-grafana-role.sh"
|
||||
|
||||
# См. обоснование shutil.which в test_2203_backup_trailer_grep_dashdash.py:
|
||||
# голое "bash" на Windows с WSL резолвится в System32\bash.exe.
|
||||
BASH = shutil.which("bash")
|
||||
if BASH is None: # pragma: no cover - окружение без bash не запустит эти тесты
|
||||
pytest.skip("bash не найден в PATH — тест требует shell-исполнения", allow_module_level=True)
|
||||
|
||||
# Подставной `docker`: отвечает за контейнер, отдаёт заданный POSTGRES_USER и
|
||||
# принимает psql только от роли PRIV_ROLE. Код 28 не нужен — psql на отказе
|
||||
# просто выходит ненулём, как в проде при неверной роли.
|
||||
_DOCKER_STUB = r"""#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
inspect) exit 0 ;;
|
||||
exec)
|
||||
args="$*"
|
||||
case "$args" in
|
||||
*"printf %s"*) printf '%s' "${PG_USER_STUB:-}"; exit 0 ;;
|
||||
esac
|
||||
u=""; prev=""
|
||||
for a in "$@"; do
|
||||
if [ "$prev" = "-U" ]; then u="$a"; break; fi
|
||||
prev="$a"
|
||||
done
|
||||
case "$args" in
|
||||
*-tAc*)
|
||||
if [ "$u" = "${PRIV_ROLE:-}" ]; then echo 1; exit 0; fi
|
||||
exit 1 ;;
|
||||
*)
|
||||
cat >/dev/null 2>&1 || true
|
||||
if [ "$u" = "${PRIV_ROLE:-}" ]; then exit 0; fi
|
||||
exit 1 ;;
|
||||
esac ;;
|
||||
esac
|
||||
exit 0
|
||||
"""
|
||||
|
||||
_HARNESS = r"""
|
||||
set -u
|
||||
stubdir=$(mktemp -d)
|
||||
cat > "$stubdir/docker" <<'STUB'
|
||||
@@STUB@@
|
||||
STUB
|
||||
chmod +x "$stubdir/docker"
|
||||
export PATH="$stubdir:$PATH"
|
||||
export PG_USER_STUB='@@PG_USER@@'
|
||||
export PRIV_ROLE='@@PRIV@@'
|
||||
export GLITCHTIP_RO_PASSWORD='stub-pass'
|
||||
bash '@@SCRIPT@@'
|
||||
echo "RC=$?"
|
||||
"""
|
||||
|
||||
|
||||
def _run(pg_user: str, priv_role: str) -> tuple[str, str]:
|
||||
"""Гоняет РЕАЛЬНЫЙ скрипт: контейнер сообщает pg_user, привилегии есть у priv_role."""
|
||||
assert SCRIPT.is_file(), f"нет {SCRIPT} — скрипт переехал, гейт ослеп"
|
||||
harness = (
|
||||
_HARNESS.replace("@@STUB@@", _DOCKER_STUB)
|
||||
.replace("@@PG_USER@@", pg_user)
|
||||
.replace("@@PRIV@@", priv_role)
|
||||
.replace("@@SCRIPT@@", SCRIPT.as_posix())
|
||||
)
|
||||
proc = subprocess.run([BASH, "-c", harness], capture_output=True, timeout=30)
|
||||
return (
|
||||
proc.stdout.decode("utf-8", errors="replace"),
|
||||
proc.stderr.decode("utf-8", errors="replace"),
|
||||
)
|
||||
|
||||
|
||||
def test_picks_role_reported_by_container() -> None:
|
||||
"""Прод-случай: роль `infra`, которой нет ни в одном угадываемом имени.
|
||||
|
||||
Ядро регресса — до фикса скрипт перебирал glitchtip/forgejo/postgres,
|
||||
получал отказ на всех трёх и выходил с ошибкой.
|
||||
"""
|
||||
out, err = _run(pg_user="infra", priv_role="infra")
|
||||
assert "привилегированная роль: infra" in out, f"роль из контейнера не выбрана.\n{out}\n{err}"
|
||||
assert "RC=0" in out, f"скрипт должен отработать успешно.\nstdout:\n{out}\nstderr:\n{err}"
|
||||
assert "не нашёл роль" not in err
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pg_user", ["gendesign", "tradein"])
|
||||
def test_works_for_other_project_clusters(pg_user: str) -> None:
|
||||
"""Два других кластера проекта — их имена тоже не входили в перебор."""
|
||||
out, _err = _run(pg_user=pg_user, priv_role=pg_user)
|
||||
assert f"привилегированная роль: {pg_user}" in out
|
||||
|
||||
|
||||
def test_falls_back_to_name_list_when_variable_is_empty() -> None:
|
||||
"""Кластер не из образа postgres: POSTGRES_USER пуст → работает прежний перебор.
|
||||
|
||||
Инвариант: фикс ДОБАВЛЯЕТ источник истины, а не отменяет запасной путь.
|
||||
"""
|
||||
out, _err = _run(pg_user="", priv_role="forgejo")
|
||||
assert "привилегированная роль: forgejo" in out, f"запасной перебор сломан.\n{out}"
|
||||
|
||||
|
||||
def test_still_fails_loudly_when_no_role_has_the_right() -> None:
|
||||
"""Когда привилегий нет ни у кого — по-прежнему громкая ошибка, а не тихий успех."""
|
||||
out, err = _run(pg_user="nobody", priv_role="__никто__")
|
||||
assert "не нашёл роль с правом CREATE ROLE" in err, f"ошибка должна остаться громкой.\n{err}"
|
||||
assert "RC=0" not in out, "скрипт не должен рапортовать успех, не создав роль"
|
||||
|
||||
|
||||
def test_every_setup_script_the_workflow_runs_is_also_a_trigger() -> None:
|
||||
"""Скрипт, который деплой запускает, обязан заводить этот же деплой.
|
||||
|
||||
Иначе правка скрипта не вызывает выкат, и на хосте молча остаётся старая
|
||||
версия — тот же класс, что #2203 закрыл глобом `ops/*.sh`. Конкретно здесь
|
||||
в `paths:` стоял только `setup-metrics-secrets.sh`, а запускались три
|
||||
скрипта.
|
||||
"""
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
wf = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
|
||||
assert wf.is_file(), f"нет {wf} — воркфлоу переехал, гейт ослеп"
|
||||
text = wf.read_text(encoding="utf-8")
|
||||
|
||||
invoked = set(re.findall(r"bash\s+(scripts/setup-metrics-[\w-]+\.sh)", text))
|
||||
assert invoked, "не нашёл ни одного запускаемого setup-скрипта — изменился синтаксис вызова"
|
||||
|
||||
patterns = re.findall(r'^\s+-\s+"([^"]+)"\s*$', text, re.M)
|
||||
assert patterns, "не нашёл ни одного paths-шаблона"
|
||||
|
||||
for script in sorted(invoked):
|
||||
assert any(fnmatch.fnmatch(script, p) for p in patterns), (
|
||||
f"{script} запускается деплоем, но не входит ни в один шаблон paths: {patterns}"
|
||||
)
|
||||
|
|
@ -25,8 +25,25 @@ fi
|
|||
# Ищем роль с правом заводить других. Имя суперпользователя в этом кластере
|
||||
# нигде не зафиксировано, а угадывать «postgres» неверно: в образе оно задаётся
|
||||
# переменной POSTGRES_USER и здесь ею не является.
|
||||
#
|
||||
# Поэтому НЕ угадываем, а спрашиваем сам контейнер: POSTGRES_USER — это та самая
|
||||
# переменная, которой роль и была создана при initdb, то есть источник истины.
|
||||
# Прежний список был перебором трёх имён (glitchtip/forgejo/postgres), и ни одно
|
||||
# из них не совпадало ни с одним реальным кластером проекта:
|
||||
#
|
||||
# gendesign-infra-postgres → infra
|
||||
# gendesign-postgres-1 → gendesign
|
||||
# tradein-postgres → tradein
|
||||
#
|
||||
# Из-за этого деплой стека падал на первом же прогоне (#3099, задача 23657):
|
||||
# ОШИБКА: не нашёл роль с правом CREATE ROLE в gendesign-infra-postgres
|
||||
# Замер 26.08 на живом контейнере: glitchtip/forgejo/postgres — отказ,
|
||||
# infra — проходит. Список имён оставлен ПОСЛЕ него запасным путём: он
|
||||
# пригодится кластеру, поднятому не из образа postgres, где POSTGRES_USER пуст.
|
||||
SUPER=""
|
||||
for candidate in glitchtip forgejo postgres; do
|
||||
_pg_user="$(docker exec "$CONT" sh -c 'printf %s "${POSTGRES_USER:-}"' 2>/dev/null || true)"
|
||||
for candidate in $_pg_user glitchtip forgejo postgres; do
|
||||
[ -n "$candidate" ] || continue
|
||||
if docker exec "$CONT" psql -U "$candidate" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_roles WHERE rolname = CURRENT_USER AND (rolsuper OR rolcreaterole)" 2>/dev/null \
|
||||
| grep -q 1; then
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue