gendesign/backend/tests/ops/test_3078_metrics_role_discovery.py
bot-backend 33bc7e4acf
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
fix(observability): привилегированную роль спрашиваем у контейнера, а не угадываем (#3078)
Стек наблюдаемости не поднялся ни на одном хосте после мержа #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.
2026-08-26 12:08:37 +03:00

181 lines
8.8 KiB
Python
Raw Permalink 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.

"""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}"
)