From 5ea05cffa624bc7da54891a15d8820575cc5e702 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 27 Aug 2026 15:10:23 +0300 Subject: [PATCH] =?UTF-8?q?fix(ops/metrics):=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B0=20Ale?= =?UTF-8?q?rtmanager=20=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B5=D0=B7=D0=B6=D0=B0=D0=BB=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пойман на проде 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 чист. --- .forgejo/workflows/deploy-metrics.yml | 29 +++++++ .../tests/ops/test_3xxx_alertmanager_inode.py | 82 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 backend/tests/ops/test_3xxx_alertmanager_inode.py diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 214cdc3f..ed17c6be 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -205,6 +205,10 @@ jobs: exit 1 fi echo "Алерты: канал задан, конфиг проверен, Alertmanager поднимается." + # Конфиг перерисован — значит у файла НОВЫЙ инод (см. rm выше). + # Помечаем, чтобы ниже пересоздать контейнер: почему это + # обязательно — объяснено у самого пересоздания. + ALERTMANAGER_RERENDERED=1 else echo "::warning title=Алерты выключены::METRICS_TELEGRAM_BOT_TOKEN/CHAT_ID не заданы. Метрики и логи собираются, но при срабатывании правила НИКТО не будет уведомлён. Канал доставки — открытый вопрос #3078." fi @@ -219,6 +223,31 @@ jobs: COMPOSE_PROFILES="$PROFILES" \ docker compose -p gendesign-metrics -f docker-compose.metrics.yml up -d --remove-orphans + # ── Alertmanager: пересоздать, если конфиг перерисовали ───────── + # `up -d` выше СЧИТАЕТ alertmanager неизменившимся: он сравнивает + # описание сервиса, а содержимое бинд-маунта в это сравнение не + # входит. Контейнер продолжает работать — и продолжает держать + # СТАРЫЙ инод файла: `rm` при рендере не правит файл на месте, а + # создаёт новый, и открытый дескриптор внутри контейнера смотрит + # на прежний, уже удалённый. + # + # Отказ полностью беззвучный и оттого злой. На диске лежит новый + # конфиг, `amtool check-config` его проверяет и одобряет, деплой + # зелёный — а маршрутизация работает по старому. Пойман на проде + # 27.08: после #3136 на диске уже стоял `webhook_configs` на + # alert-ack, а контейнер всё ещё слал напрямую в Telegram. Значит + # и прежние правки маршрутов доезжали лишь тогда, когда контейнер + # пересоздавался по другой причине. + # + # Перезагрузка по SIGHUP/API не помогает: она перечитывает тот же + # открытый инод. Помогает только пересоздание контейнера. + if [ "${ALERTMANAGER_RERENDERED:-0}" = "1" ]; then + COMPOSE_PROFILES="$PROFILES" \ + docker compose -p gendesign-metrics -f docker-compose.metrics.yml \ + up -d --force-recreate alertmanager + echo "Alertmanager: контейнер пересоздан — иначе читал бы конфиг по старому иноду." + fi + # ── Caddy: СНАЧАЛА проверить, потом применять ────────────────── # На этом хосте тот же Caddy обслуживает git., errors. и obsidian. # Синтаксическая ошибка в infra.caddy положила бы их все, включая diff --git a/backend/tests/ops/test_3xxx_alertmanager_inode.py b/backend/tests/ops/test_3xxx_alertmanager_inode.py new file mode 100644 index 00000000..e5b34842 --- /dev/null +++ b/backend/tests/ops/test_3xxx_alertmanager_inode.py @@ -0,0 +1,82 @@ +"""Правки конфига 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" + ) -- 2.45.3