gendesign/backend/tests/ops/test_3xxx_alertmanager_inode.py
bot-backend 5ea05cffa6
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m28s
fix(ops/metrics): правки конфига Alertmanager молча не доезжали до контейнера
Пойман на проде 27.08 сразу после мержа #3136. На диске лежал новый конфиг —
`webhook_configs` на сервис кнопки подтверждения, — `amtool check-config` его
одобрил, деплой зелёный. А контейнер продолжал слать алерты напрямую:

    на диске:     webhook_configs: url http://alert-ack:8080/alertmanager
    в контейнере: telegram_configs:

Конфиг подключён бинд-маунтом ФАЙЛА, а рендер делает `rm` и создаёт файл
заново — иначе не перезаписать: после chown он принадлежит 65534 с правами
600, а каталог принадлежит деплой-пользователю. `rm` + создание даёт НОВЫЙ
инод, тогда как открытый дескриптор внутри работающего контейнера продолжает
смотреть на прежний, уже удалённый. `up -d` контейнер не трогает: он
сравнивает описание сервиса, а содержимое бинд-маунта в сравнение не входит.

Отказ беззвучный — ни одного красного признака нигде. Значит и все прежние
правки маршрутизации применялись лишь тогда, когда контейнер пересоздавался
по совпадению.

Перезагрузка по SIGHUP/API не лечит: она перечитывает тот же открытый инод.
Лечит только пересоздание контейнера — его и добавляю, под флагом, который
выставляется ПОСЛЕ успешной проверки конфига. Порядок важен: при обратном
битый конфиг убивал бы работающий Alertmanager вместо того, чтобы оставить
прежний работать.

Прод уже приведён в соответствие вручную — контейнер пересоздан, маршрут
клиентских инцидентов теперь идёт через кнопку. Эта правка нужна, чтобы
следующая правка конфига доехала сама.

Три теста: пересоздание есть, оно закрыто проверкой флага (безусловное рвало
бы доставку на каждом деплое метрик), флаг выставляется после проверки.
Прогон: 78 ops-тестов зелёные, ruff чист.
2026-08-27 15:10:23 +03:00

82 lines
4.4 KiB
Python
Raw 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.

"""Правки конфига Alertmanager обязаны доезжать до работающего контейнера.
ЧТО СЛУЧИЛОСЬ НА ПРОДЕ 27.08. После мержа #3136 на диске лежал уже новый
конфиг — `webhook_configs` на сервис кнопки подтверждения, — `amtool
check-config` его одобрил, деплой был зелёный. А контейнер продолжал слать
алерты напрямую в Telegram:
на диске: - name: telegram-clients
webhook_configs:
- url: "http://alert-ack:8080/alertmanager"
в контейнере: - name: telegram-clients
telegram_configs:
ПОЧЕМУ. Конфиг подключён бинд-маунтом ФАЙЛА, а рендер в деплое делает `rm` и
создаёт файл заново (иначе не перезаписать: после chown он принадлежит 65534 с
правами 600). `rm` + создание — это НОВЫЙ инод, а открытый дескриптор внутри
работающего контейнера продолжает смотреть на прежний, уже удалённый. `up -d`
контейнер не трогает: он сравнивает описание сервиса, а содержимое бинд-маунта
в сравнение не входит.
Отказ беззвучный: ни одного красного признака нигде. Значит и прежние правки
маршрутов применялись лишь тогда, когда контейнер пересоздавался по совпадению.
Перезагрузка по SIGHUP/API не лечит — она перечитывает тот же инод. Лечит
только пересоздание контейнера, и именно его наличие здесь и проверяется.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
def _text() -> str:
return WORKFLOW.read_text(encoding="utf-8")
def test_rerendered_config_forces_container_recreate() -> None:
"""После перерисовки конфига контейнер обязан пересоздаваться."""
text = _text()
assert "--force-recreate alertmanager" in text, (
"нет пересоздания Alertmanager — правка конфига не доедет до "
"работающего контейнера, он останется на старом иноде"
)
def test_recreate_is_guarded_by_the_rerender_flag() -> None:
"""Пересоздание — только когда конфиг действительно перерисовали.
Безусловное пересоздание на каждом деплое метрик рвало бы доставку алертов
там, где ничего не менялось.
"""
text = _text()
assert "ALERTMANAGER_RERENDERED=1" in text, "флаг перерисовки не выставляется"
guard = re.search(
r'if \[ "\$\{ALERTMANAGER_RERENDERED:-0\}" = "1" \]; then(.+?)\bfi\b',
text,
re.S,
)
assert guard, "пересоздание не закрыто проверкой флага"
assert "--force-recreate alertmanager" in guard.group(1), (
"пересоздание стоит вне проверки флага — оно будет выполняться всегда"
)
def test_flag_is_set_only_after_config_passed_validation() -> None:
"""Флаг выставляется ПОСЛЕ проверки конфига, а не до неё.
Иначе битый конфиг пересоздавал бы живой контейнер и ронял алертинг целиком
вместо того, чтобы оставить работать прежний.
"""
text = _text()
check = text.index("check-config /tmp/am.yml")
flag = text.index("ALERTMANAGER_RERENDERED=1")
assert check < flag, (
"флаг перерисовки выставлен раньше проверки конфига — битый конфиг "
"убил бы работающий Alertmanager"
)