Compare commits
No commits in common. "1da2eee1424bcfd6f7ed50590107888cb1dee40e" and "f9fc07aea3cdf9120fb4ee6816138a06f07fe29a" have entirely different histories.
1da2eee142
...
f9fc07aea3
3 changed files with 2 additions and 204 deletions
|
|
@ -22,11 +22,7 @@ on:
|
||||||
- "caddy/sites/infra.caddy"
|
- "caddy/sites/infra.caddy"
|
||||||
- "caddy/metrics-ui.caddy.snippet"
|
- "caddy/metrics-ui.caddy.snippet"
|
||||||
- "caddy/metrics-ingest.caddy.snippet"
|
- "caddy/metrics-ingest.caddy.snippet"
|
||||||
# Глоб, а не точечный `setup-metrics-secrets.sh` (#2203: класс бага, а не
|
- "scripts/setup-metrics-secrets.sh"
|
||||||
# один файл). Деплой запускает ТРИ setup-скрипта — secrets, grafana-role и
|
|
||||||
# exporter-dsn, — а в триггере стоял только первый: правка двух остальных
|
|
||||||
# не заводила выкат, и на хосте продолжала исполняться старая версия молча.
|
|
||||||
- "scripts/setup-metrics-*.sh"
|
|
||||||
- ".forgejo/workflows/deploy-metrics.yml"
|
- ".forgejo/workflows/deploy-metrics.yml"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
"""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,25 +25,8 @@ fi
|
||||||
# Ищем роль с правом заводить других. Имя суперпользователя в этом кластере
|
# Ищем роль с правом заводить других. Имя суперпользователя в этом кластере
|
||||||
# нигде не зафиксировано, а угадывать «postgres» неверно: в образе оно задаётся
|
# нигде не зафиксировано, а угадывать «postgres» неверно: в образе оно задаётся
|
||||||
# переменной POSTGRES_USER и здесь ею не является.
|
# переменной 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=""
|
SUPER=""
|
||||||
_pg_user="$(docker exec "$CONT" sh -c 'printf %s "${POSTGRES_USER:-}"' 2>/dev/null || true)"
|
for candidate in glitchtip forgejo postgres; do
|
||||||
for candidate in $_pg_user glitchtip forgejo postgres; do
|
|
||||||
[ -n "$candidate" ] || continue
|
|
||||||
if docker exec "$CONT" psql -U "$candidate" -d postgres -tAc \
|
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 \
|
"SELECT 1 FROM pg_roles WHERE rolname = CURRENT_USER AND (rolsuper OR rolcreaterole)" 2>/dev/null \
|
||||||
| grep -q 1; then
|
| grep -q 1; then
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue