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.
181 lines
8.8 KiB
Python
181 lines
8.8 KiB
Python
"""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}"
|
||
)
|