From c31aff68a9636b7b232712a46b0b46aca8b79301 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 15:51:20 +0500 Subject: [PATCH 1/2] =?UTF-8?q?=D0=94=D0=B5=D0=BF=D0=BB=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D0=BA=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D1=8E,?= =?UTF-8?q?=20=D0=B0=20=D0=BD=D0=B5=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=BB=D0=B0=D0=B4=D1=91=D1=82=20=D0=B5=D1=91=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=B8=D1=81=D0=BA=20(#3467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правки ops/metrics/prometheus/** доезжали до диска и не вступали в силу: контейнер Prometheus не перезапускался с 2026-08-27, а деплой перезагружал один Caddy. Правила из #3464 лежали и на диске, и внутри контейнера, а /api/v1/rules отдавал прежние — при зелёном деплое и без единого отрицательного признака. - проверка promtool (check config + check rules) одноразовым контейнером того же образа по файлам с диска, затем POST /-/reload; провал проверки роняет шаг и перезагрузки не делает — работающий Prometheus остаётся на прежней конфигурации; - сверка инода у одиночных файловых маунтов (prometheus.yml, loki-config.yml, alert-ack/app.py) с пересозданием контейнера: маунт ОДНОГО файла держит старый инод, а у Loki ручки перезагрузки нет вовсе (404 на /reload); - датасорсы Grafana перечитываются штатной ручкой: их провиженинг применяется только при старте, дашборды же провайдер пересканирует сам; - гейт backend/tests/ops/test_3467_prometheus_reload.py краснеет, если перезагрузка исчезла, оторвалась от проверки конфигурации или у нового файлового маунта нет пути доезда. Closes #3467 Co-Authored-By: Claude Opus 5 --- .forgejo/workflows/deploy-metrics.yml | 98 +++++++++- .../tests/ops/test_3467_prometheus_reload.py | 184 ++++++++++++++++++ 2 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 backend/tests/ops/test_3467_prometheus_reload.py diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 559784aa..9b52d245 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -317,6 +317,70 @@ jobs: echo "Alertmanager: контейнер пересоздан — иначе читал бы конфиг по старому иноду." fi + # ── Prometheus: СНАЧАЛА проверить конфигурацию ───────────────── + # Тот же порядок, что у Caddy ниже: проверка — здесь, применение — + # только после неё (сама перезагрузка стоит в конце, после приёмки: + # дёргать ручку раньше, чем процесс отвечает, нечем). + # + # ЗАЧЕМ ЭТО ВООБЩЕ. Правила подключены КАТАЛОГОМ, поэтому новый + # текст контейнер видит сразу — но Prometheus читает правила только + # при старте либо по явной перезагрузке, а деплой не делал ни того, + # ни другого. 12.09: контейнер работал с 27.08, на диске (и внутри + # контейнера) лежали правила из #3464, а /api/v1/rules отдавал + # прежние. Зелёный деплой, новый файл на диске, старое поведение — + # отказ без единого отрицательного признака. Значит и все прежние + # правки ops/metrics/prometheus/** вступали в силу лишь тогда, когда + # контейнер пересоздавался по какой-то посторонней причине. + # + # Проверяем ОДНОРАЗОВЫМ контейнером того же образа по файлам С + # ДИСКА, а не `docker exec` в работающий, по двум причинам: + # 1) prometheus.yml подключён бинд-маунтом ОДНОГО файла, и + # работающий контейнер после `git reset` читает СТАРЫЙ инод — + # exec проверял бы не тот текст, который применится; + # 2) битый конфиг обязан остановить деплой ДО пересоздания + # контейнера, иначе Prometheus не поднимется вовсе. + # + # Нужны ОБЕ команды: при пустом (или не смонтированном) каталоге + # правил `check config` печатает «0 rule files found» и проходит, а + # `check rules` по явному шаблону в этом случае падает — проверено + # на стенде prom/prometheus:v3.1.0. + PROM_IMAGE=$(grep -oE 'prom/prometheus:[^ ]+' docker-compose.metrics.yml | head -1) + if ! docker run --rm -v "$PWD/ops/metrics/prometheus:/etc/prometheus:ro" \ + --entrypoint sh "$PROM_IMAGE" -c \ + 'promtool check config /etc/prometheus/prometheus.yml && promtool check rules /etc/prometheus/rules/*.yml'; then + echo "ОШИБКА: конфиг или правила Prometheus не проходят проверку — перезагрузка НЕ выполнена." + echo "Работающий Prometheus не тронут и остаётся на прежней конфигурации. Чинить и повторять." + exit 1 + fi + + # ── Бинд-маунт ОДНОГО файла держит инод ──────────────────────── + # Та же ловушка, что у Alertmanager выше и у Alloy в джобах агентов: + # `git reset --hard` пишет изменившийся файл НОВЫМ инодом, а + # работающий контейнер продолжает читать прежний, уже удалённый. + # `up -d` этого не видит (сравнивается описание сервиса, не + # содержимое маунта), перезагрузка по API — тоже (перечитывается тот + # же открытый инод). Лечит только пересоздание контейнера. + # + # Список обязан совпадать с одиночными файловыми маунтами + # docker-compose.metrics.yml; сторожит соответствие + # backend/tests/ops/test_3467_prometheus_reload.py. Производные + # файлы (alertmanager.yml, alertmanager_targets.gen.yml) сюда не + # входят: их рендерит сам деплой и уже разбирается с инодом. + for entry in \ + "prometheus gendesign-prometheus ops/metrics/prometheus/prometheus.yml /etc/prometheus/prometheus.yml" \ + "loki gendesign-loki ops/metrics/loki/loki-config.yml /etc/loki/loki-config.yml" \ + "alert-ack gendesign-alert-ack ops/metrics/alert-ack/app.py /app/app.py"; do + set -- $entry + docker ps -q -f "name=^$2\$" | grep -q . || continue + if [ "$(stat -c %i "$3")" = "$(docker exec "$2" stat -c %i "$4")" ]; then + continue + fi + COMPOSE_PROFILES="$PROFILES" \ + docker compose -p gendesign-metrics -f docker-compose.metrics.yml \ + up -d --force-recreate "$1" + echo "$1: $3 переписан новым инодом — контейнер пересоздан." + done + # ── Caddy: СНАЧАЛА проверить, потом применять ────────────────── # На этом хосте тот же Caddy обслуживает git., errors. и obsidian. # Синтаксическая ошибка в infra.caddy положила бы их все, включая @@ -337,13 +401,45 @@ jobs: # ── Приёмка ──────────────────────────────────────────────────── for i in $(seq 1 30); do - if docker exec gendesign-prometheus wget -q --spider http://localhost:9090/-/healthy 2>/dev/null; then + if docker exec gendesign-prometheus wget -q --spider http://localhost:9090/-/healthy 2>/dev/null \ + && docker exec gendesign-grafana wget -q --spider http://localhost:3000/api/health 2>/dev/null; then break fi sleep 3 done docker compose -p gendesign-metrics -f docker-compose.metrics.yml ps + # ── Применение: перечитать конфигурацию работающими сервисами ─── + # Конфиг проверен выше, контейнеры отвечают — только теперь. + # + # Prometheus перечитывает и prometheus.yml, и правила по POST + # /-/reload (флаг --web.enable-lifecycle стоит в + # docker-compose.metrics.yml). Без флага ручка отвечает 405 и шаг + # краснеет — это и нужно: молча оставаться на старых правилах + # деплой больше не должен. + docker exec gendesign-prometheus wget -q -O- --post-data='' \ + http://localhost:9090/-/reload + echo "Prometheus: конфигурация перечитана, правила вступили в силу." + + # Grafana. Дашборды провайдер пересканирует сам раз в 30 с + # (updateIntervalSeconds в provisioning/dashboards/dashboards.yml — + # проверено на стенде grafana:11.5.1: новый файл появился без + # рестарта), а ДАТАСОРСЫ применяются только при старте: правка + # provisioning/datasources лежала бы на диске без эффекта — ровно + # тот же разрыв, что и у правил Prometheus. Штатная ручка + # перезагрузки дешевле рестарта и не роняет открытые дашборды. + # + # Пароль берётся из окружения САМОГО контейнера: в аргументы команд + # на хосте и в лог деплоя он так не попадает. + if docker exec gendesign-grafana sh -c \ + 'wget -q -O- --post-data="" --header="Authorization: Basic $(printf "%s:%s" "${GF_SECURITY_ADMIN_USER:-admin}" "${GF_SECURITY_ADMIN_PASSWORD}" | base64 -w0)" http://localhost:3000/api/admin/provisioning/datasources/reload'; then + echo + echo "Grafana: датасорсы перечитаны." + else + echo "ОШИБКА: Grafana не перечитала датасорсы — правка provisioning/datasources осталась бы без эффекта." + exit 1 + fi + # ═══ АГЕНТЫ — оба хоста ═══════════════════════════════════════════════════ agent-apps: runs-on: ubuntu-latest diff --git a/backend/tests/ops/test_3467_prometheus_reload.py b/backend/tests/ops/test_3467_prometheus_reload.py new file mode 100644 index 00000000..547433da --- /dev/null +++ b/backend/tests/ops/test_3467_prometheus_reload.py @@ -0,0 +1,184 @@ +"""Деплой метрик обязан не только положить конфигурацию на диск, но и применить её. + +ЧТО СЛУЧИЛОСЬ НА ПРОДЕ 12.09 (#3467). #3464 поправил два правила в +`ops/metrics/prometheus/rules/infra.yml`, «Deploy Metrics / server» на этом +коммите — success, новый текст лежал и на диске инфраструктурного хоста, и +ВНУТРИ контейнера: + + docker exec gendesign-prometheus grep -n container_memory_working_set_bytes \ + /etc/prometheus/rules/infra.yml → строка 139, новая формулировка + +а API отдавал прежнее правило: + + GET /api/v1/rules → ContainerNearMemoryLimit: + container_spec_memory_limit_bytes{name!=""} > 0 and ... + +ПОЧЕМУ. Правила подключены КАТАЛОГОМ, поэтому новый текст контейнер видит сразу, +но Prometheus читает правила только при старте или по явной перезагрузке. +Контейнер не перезапускался с 2026-08-27T18:12:24Z (16 суток), а деплой +перезагружал один Caddy. То есть ЛЮБАЯ правка ops/metrics/prometheus/** с +момента появления стека доезжала до диска и не вступала в силу до случайного +пересоздания контейнера — при зелёном деплое и без единого отрицательного +признака. Тот же класс, что #3448: объявлено ≠ исполняется. + +ЧТО ПРОВЕРЕНО НА СТЕНДЕ, а не взято из документации (prom/prometheus:v3.1.0, +те же файлы, что на проде): + + правило переписано на диске, контейнер не тронут → API отдаёт СТАРОЕ + promtool check config && check rules → rc=0, затем POST /-/reload → rc=0 + после reload → API отдаёт НОВОЕ + битое правило → проверка rc=1 (reload не делается) + пустой каталог правил → `check config` ПРОХОДИТ («0 rule files found»), + падает только `check rules` — потому в деплое стоят + обе команды, а не одна + +Тест сторожит связку целиком: правки правил приезжают этим воркфлоу (триггер), +перезагрузка в нём есть, она стоит ПОСЛЕ проверки конфигурации, а провалившаяся +проверка обрывает деплой и не доходит до перезагрузки. Отдельно проверяется, что +у каждого одиночного файлового маунта есть путь доезда: такой маунт держит инод, +и перезагрузка по API его не лечит (см. test_3xxx_alertmanager_inode.py). +""" + +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" +COMPOSE = REPO_ROOT / "docker-compose.metrics.yml" + +# Перезагрузка Prometheus: POST на ручку lifecycle из работающего контейнера. +RELOAD = re.compile( + r"docker exec gendesign-prometheus wget[^\n]*--post-data[^\n]*" + r"http://localhost:9090/-/reload" +) +# Проверка конфигурации: обе команды promtool по файлам с диска. +CHECK = re.compile( + r"promtool check config \S*prometheus\.yml" + r" && promtool check rules \S*rules/\*\.yml" +) +# Ветка «проверка не прошла» — от `if ! ... promtool ...` до её `fi`. +CHECK_FAILED_BRANCH = re.compile(r"if ! docker run[^\n]*promtool[^\n]*; then(.+?)\n\s*fi\n", re.S) +# Одиночный файловый маунт в серверном стеке: ./ops/metrics/... : /путь/в/контейнере +MOUNT = re.compile(r"-\s+\./(ops/metrics/\S+?):(/\S+?):ro") + + +def _server_script() -> str: + """Скрипт серверной джобы с ЗАКЛЕЕННЫМИ переносами строк. + + Команды в воркфлоу разбиты `\\` по ширине, и без склейки любой поиск + «команда целиком» ловил бы только первую строку. + """ + text = WORKFLOW.read_text(encoding="utf-8") + start = text.index("Поднять серверный стек") + end = text.index("# ═══ АГЕНТЫ") + return re.sub(r"\s*\\\n\s*", " ", text[start:end]) + + +def test_rules_are_delivered_by_this_workflow() -> None: + """Правки правил приезжают на хост именно этим деплоем. + + Без этого остальные проверки сторожили бы механизм, который никто не + запускает: «объявлено» и «исполняется» разошлись бы уже на триггере. + """ + text = WORKFLOW.read_text(encoding="utf-8") + paths = text[text.index("paths:") : text.index("workflow_dispatch")] + assert '"ops/metrics/**"' in paths, ( + "деплой не запускается на правку ops/metrics/** — правила меняются мимо него" + ) + + +def test_deploy_reloads_prometheus() -> None: + """Перезагрузка Prometheus в деплое есть. + + Её отсутствие и есть #3467: конфигурация на диске новая, работающий процесс + на старой, деплой зелёный. + """ + assert RELOAD.search(_server_script()), ( + "в серверной джобе нет POST /-/reload для Prometheus — правки правил " + "лягут на диск и не вступят в силу, как это и было с #3464" + ) + + +def test_reload_comes_after_config_check() -> None: + """Перезагрузка — только после проверки конфигурации, как у Caddy. + + Порядок здесь несущий: перезагрузка битого конфига оставляет Prometheus на + прежнем — то есть ровно тот же тихий отказ, который чинится. + """ + script = _server_script() + check = CHECK.search(script) + reload_ = RELOAD.search(script) + assert check, "в деплое нет проверки promtool (check config + check rules)" + assert reload_, "в деплое нет перезагрузки Prometheus" + assert check.start() < reload_.start(), ( + "перезагрузка стоит РАНЬШЕ проверки конфигурации — битый конфиг молча " + "оставит Prometheus на старой конфигурации при зелёном деплое" + ) + + +def test_failed_check_stops_deploy_without_reload() -> None: + """Провалившаяся проверка обрывает деплой и НЕ перезагружает Prometheus.""" + branch = CHECK_FAILED_BRANCH.search(_server_script()) + assert branch, "проверка promtool не закрыта веткой обработки отказа" + body = branch.group(1) + assert "exit 1" in body, "при провале проверки деплой продолжается — отказ выглядел бы успехом" + assert "/-/reload" not in body, "в ветке отказа делается перезагрузка" + + +def test_lifecycle_flag_makes_reload_possible() -> None: + """Ручка /-/reload работает только с --web.enable-lifecycle. + + Флаг и перезагрузка — одно целое: снимут флаг, и POST начнёт отвечать 405. + """ + assert "--web.enable-lifecycle" in COMPOSE.read_text(encoding="utf-8"), ( + "у Prometheus снят --web.enable-lifecycle — перезагрузка из деплоя невозможна" + ) + + +def test_every_single_file_mount_has_a_way_to_arrive() -> None: + """У каждого одиночного файлового маунта есть путь доезда до контейнера. + + Бинд-маунт ОДНОГО файла держит инод: `git reset --hard` пишет новый файл, а + контейнер продолжает читать прежний — `up -d` этого не видит, перезагрузка + по API тоже (перечитывается тот же инод). Лечит только пересоздание, и + деплой обязан сверять инод по каждому такому файлу. + + Список берётся ИЗ compose, а не из воркфлоу: добавят новый файловый маунт — + тест покраснеет, пока для него не появится сверка. Производные файлы + (alertmanager.yml, alertmanager_targets.gen.yml) в git не лежат и потому + сюда не попадают: их рендерит сам деплой и сам разбирается с инодом. + """ + script = _server_script() + mounts = [ + (host, container) + for host, container in MOUNT.findall(COMPOSE.read_text(encoding="utf-8")) + if (REPO_ROOT / host).is_file() + ] + assert len(mounts) == 3, ( + f"ожидались три одиночных файловых маунта (prometheus.yml, loki-config.yml, " + f"alert-ack/app.py), найдено: {mounts}" + ) + assert "stat -c %i" in script and '--force-recreate "$1"' in script, ( + "в деплое нет сверки инода с пересозданием контейнера" + ) + for host, container in mounts: + assert re.search(rf'"[^"\n]*{re.escape(host)} {re.escape(container)}[^"\n]*"', script), ( + f"{host} не сверяется с {container}: правка этого файла ляжет на диск, " + f"а контейнер останется на старом иноде — молча" + ) + + +def test_grafana_datasources_are_reloaded() -> None: + """Датасорсы Grafana применяются ТОЛЬКО при старте — их надо перечитать явно. + + Замер на стенде (grafana/grafana:11.5.1, provisioning из этого репозитория): + новый дашборд появился сам за 30 с (updateIntervalSeconds), а изменённый url + датасорса не применился и через 75 с; POST на + /api/admin/provisioning/datasources/reload применил его сразу. + """ + assert "/api/admin/provisioning/datasources/reload" in _server_script(), ( + "деплой не перечитывает датасорсы Grafana — правка " + "provisioning/datasources останется без эффекта при зелёном деплое" + ) -- 2.45.3 From 3aef5c363173317713347100d9020abd7508382b Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 16:18:45 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D0=93=D0=B5=D0=B9=D1=82=20#3467=20=D0=B1?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=D0=BF=20=D0=BA=20`||=20true`=20=D0=B8=20=D0=BA=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=83=D0=BD=D1=82=D1=83=20=D0=B1=D0=B5=D0=B7=20`:ro`=20(?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20PR=20#3475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мутации ревьюера проходили гейт насквозь: - к шагу `POST /-/reload` дописано `|| true` — все 7 тестов зелёные. Regex заканчивался на URL и хвост строки не смотрел; сегодня спасает `set -e`, но именно так («зелёная сводка ≠ зелёный выход») в этом репозитории уже ломали гейты. Добавлен отрицательный lookahead на `||` в той же строке; - новый одиночный файловый маунт БЕЗ суффикса `:ro` — тест зелёный, потому что распознавание маунта требовало `:ro`. Права к типу маунта отношения не имеют: инод он держит одинаково. Требование снято. Проверка promtool переехала ВЫШЕ `docker compose up -d`: прежнее место (между подъёмом и перезагрузкой) не спасало от битого конфига, потому что `up -d` пересоздаёт контейнер при смене образа. Новый тест сторожит и это. Якоря границ серверной джобы и блока триггеров читаются через find с внятным assert: при переименовании шага гейт скажет, что сломалось, а не даст ValueError. Co-Authored-By: Claude Opus 5 --- .forgejo/workflows/deploy-metrics.yml | 73 ++++++++++--------- .../tests/ops/test_3467_prometheus_reload.py | 43 +++++++++-- 2 files changed, 73 insertions(+), 43 deletions(-) diff --git a/.forgejo/workflows/deploy-metrics.yml b/.forgejo/workflows/deploy-metrics.yml index 9b52d245..1625b042 100644 --- a/.forgejo/workflows/deploy-metrics.yml +++ b/.forgejo/workflows/deploy-metrics.yml @@ -287,6 +287,43 @@ jobs: # обязан быть безопасен даже при полном доступе к дашбордам. bash scripts/setup-metrics-grafana-role.sh + # ── Prometheus: проверить конфигурацию ДО ЛЮБОГО применения ───── + # Тот же порядок, что у Caddy ниже: проверка — здесь, применение — + # только после неё. Стоит ВЫШЕ `up -d` намеренно: `up -d` + # пересоздаёт контейнер при смене образа, и тогда битый конфиг + # положил бы Prometheus раньше, чем деплой успел бы покраснеть. + # Сама перезагрузка — в конце, после приёмки: дёргать ручку + # раньше, чем процесс отвечает, нечем. + # + # ЗАЧЕМ ЭТО ВООБЩЕ. Правила подключены КАТАЛОГОМ, поэтому новый + # текст контейнер видит сразу — но Prometheus читает правила только + # при старте либо по явной перезагрузке, а деплой не делал ни того, + # ни другого. 12.09: контейнер работал с 27.08, на диске (и внутри + # контейнера) лежали правила из #3464, а /api/v1/rules отдавал + # прежние. Зелёный деплой, новый файл на диске, старое поведение — + # отказ без единого отрицательного признака. Значит и все прежние + # правки ops/metrics/prometheus/** вступали в силу лишь тогда, когда + # контейнер пересоздавался по какой-то посторонней причине. + # + # Проверяем ОДНОРАЗОВЫМ контейнером того же образа по файлам С + # ДИСКА, а не `docker exec` в работающий: prometheus.yml подключён + # бинд-маунтом ОДНОГО файла, и работающий контейнер после `git + # reset` читает СТАРЫЙ инод — exec проверял бы не тот текст, + # который применится. + # + # Нужны ОБЕ команды: при пустом (или не смонтированном) каталоге + # правил `check config` печатает «0 rule files found» и проходит, а + # `check rules` по явному шаблону в этом случае падает — проверено + # на стенде prom/prometheus:v3.1.0. + PROM_IMAGE=$(grep -oE 'prom/prometheus:[^ ]+' docker-compose.metrics.yml | head -1) + if ! docker run --rm -v "$PWD/ops/metrics/prometheus:/etc/prometheus:ro" \ + --entrypoint sh "$PROM_IMAGE" -c \ + 'promtool check config /etc/prometheus/prometheus.yml && promtool check rules /etc/prometheus/rules/*.yml'; then + echo "ОШИБКА: конфиг или правила Prometheus не проходят проверку — ничего не применяем." + echo "Работающий Prometheus не тронут и остаётся на прежней конфигурации. Чинить и повторять." + exit 1 + fi + COMPOSE_PROFILES="$PROFILES" \ docker compose -p gendesign-metrics -f docker-compose.metrics.yml pull --quiet COMPOSE_PROFILES="$PROFILES" \ @@ -317,42 +354,6 @@ jobs: echo "Alertmanager: контейнер пересоздан — иначе читал бы конфиг по старому иноду." fi - # ── Prometheus: СНАЧАЛА проверить конфигурацию ───────────────── - # Тот же порядок, что у Caddy ниже: проверка — здесь, применение — - # только после неё (сама перезагрузка стоит в конце, после приёмки: - # дёргать ручку раньше, чем процесс отвечает, нечем). - # - # ЗАЧЕМ ЭТО ВООБЩЕ. Правила подключены КАТАЛОГОМ, поэтому новый - # текст контейнер видит сразу — но Prometheus читает правила только - # при старте либо по явной перезагрузке, а деплой не делал ни того, - # ни другого. 12.09: контейнер работал с 27.08, на диске (и внутри - # контейнера) лежали правила из #3464, а /api/v1/rules отдавал - # прежние. Зелёный деплой, новый файл на диске, старое поведение — - # отказ без единого отрицательного признака. Значит и все прежние - # правки ops/metrics/prometheus/** вступали в силу лишь тогда, когда - # контейнер пересоздавался по какой-то посторонней причине. - # - # Проверяем ОДНОРАЗОВЫМ контейнером того же образа по файлам С - # ДИСКА, а не `docker exec` в работающий, по двум причинам: - # 1) prometheus.yml подключён бинд-маунтом ОДНОГО файла, и - # работающий контейнер после `git reset` читает СТАРЫЙ инод — - # exec проверял бы не тот текст, который применится; - # 2) битый конфиг обязан остановить деплой ДО пересоздания - # контейнера, иначе Prometheus не поднимется вовсе. - # - # Нужны ОБЕ команды: при пустом (или не смонтированном) каталоге - # правил `check config` печатает «0 rule files found» и проходит, а - # `check rules` по явному шаблону в этом случае падает — проверено - # на стенде prom/prometheus:v3.1.0. - PROM_IMAGE=$(grep -oE 'prom/prometheus:[^ ]+' docker-compose.metrics.yml | head -1) - if ! docker run --rm -v "$PWD/ops/metrics/prometheus:/etc/prometheus:ro" \ - --entrypoint sh "$PROM_IMAGE" -c \ - 'promtool check config /etc/prometheus/prometheus.yml && promtool check rules /etc/prometheus/rules/*.yml'; then - echo "ОШИБКА: конфиг или правила Prometheus не проходят проверку — перезагрузка НЕ выполнена." - echo "Работающий Prometheus не тронут и остаётся на прежней конфигурации. Чинить и повторять." - exit 1 - fi - # ── Бинд-маунт ОДНОГО файла держит инод ──────────────────────── # Та же ловушка, что у Alertmanager выше и у Alloy в джобах агентов: # `git reset --hard` пишет изменившийся файл НОВЫМ инодом, а diff --git a/backend/tests/ops/test_3467_prometheus_reload.py b/backend/tests/ops/test_3467_prometheus_reload.py index 547433da..4e0d51ce 100644 --- a/backend/tests/ops/test_3467_prometheus_reload.py +++ b/backend/tests/ops/test_3467_prometheus_reload.py @@ -49,9 +49,14 @@ WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml" COMPOSE = REPO_ROOT / "docker-compose.metrics.yml" # Перезагрузка Prometheus: POST на ручку lifecycle из работающего контейнера. +# +# Хвост строки смотрим НАРОЧНО: `|| true` после команды глушит её отказ, и шаг +# остаётся зелёным с неперечитанной конфигурацией — то есть ровно тот разрыв, +# который тут и чинится, только теперь ещё и с «зелёной сводкой». Ревьюер +# #3475 этой мутацией прошёл гейт насквозь. RELOAD = re.compile( r"docker exec gendesign-prometheus wget[^\n]*--post-data[^\n]*" - r"http://localhost:9090/-/reload" + r"http://localhost:9090/-/reload(?![^\n]*\|\|)" ) # Проверка конфигурации: обе команды promtool по файлам с диска. CHECK = re.compile( @@ -60,8 +65,10 @@ CHECK = re.compile( ) # Ветка «проверка не прошла» — от `if ! ... promtool ...` до её `fi`. CHECK_FAILED_BRANCH = re.compile(r"if ! docker run[^\n]*promtool[^\n]*; then(.+?)\n\s*fi\n", re.S) -# Одиночный файловый маунт в серверном стеке: ./ops/metrics/... : /путь/в/контейнере -MOUNT = re.compile(r"-\s+\./(ops/metrics/\S+?):(/\S+?):ro") +# Одиночный файловый маунт в серверном стеке: ./ops/metrics/... : /путь/в/контейнере. +# Суффикс прав (`:ro`) НЕ требуется: инод маунт держит одинаково что на чтение, +# что на запись, а требование `:ro` делало гейт слепым к маунту без него. +MOUNT = re.compile(r"-\s+\./(ops/metrics/[^\s:]+):(/[^\s:]+)") def _server_script() -> str: @@ -71,8 +78,13 @@ def _server_script() -> str: «команда целиком» ловил бы только первую строку. """ text = WORKFLOW.read_text(encoding="utf-8") - start = text.index("Поднять серверный стек") - end = text.index("# ═══ АГЕНТЫ") + start = text.find("Поднять серверный стек") + end = text.find("# ═══ АГЕНТЫ") + assert start != -1 and end != -1, ( + "в deploy-metrics.yml не найдены границы серверной джобы: шаг «Поднять " + "серверный стек» или заголовок «АГЕНТЫ» переименовали — гейт смотрел бы " + "не туда, поправь якоря здесь" + ) return re.sub(r"\s*\\\n\s*", " ", text[start:end]) @@ -83,8 +95,9 @@ def test_rules_are_delivered_by_this_workflow() -> None: запускает: «объявлено» и «исполняется» разошлись бы уже на триггере. """ text = WORKFLOW.read_text(encoding="utf-8") - paths = text[text.index("paths:") : text.index("workflow_dispatch")] - assert '"ops/metrics/**"' in paths, ( + start, end = text.find("paths:"), text.find("workflow_dispatch") + assert start != -1 and end != -1, "в deploy-metrics.yml не найден блок триггеров paths:" + assert '"ops/metrics/**"' in text[start:end], ( "деплой не запускается на правку ops/metrics/** — правила меняются мимо него" ) @@ -118,6 +131,22 @@ def test_reload_comes_after_config_check() -> None: ) +def test_config_check_precedes_any_application() -> None: + """Проверка стоит ВЫШЕ `up -d`, а не между подъёмом и перезагрузкой. + + `up -d` пересоздаёт контейнер при смене образа: проверка ниже него не + успевает спасти — битый конфиг уже применён стартом нового контейнера, и + Prometheus не поднимется вовсе. + """ + script = _server_script() + check = CHECK.search(script) + assert check, "в деплое нет проверки promtool (check config + check rules)" + assert check.start() < script.index("up -d --remove-orphans"), ( + "проверка конфигурации стоит после `up -d` — контейнер успеет " + "пересоздаться с битым конфигом раньше, чем деплой покраснеет" + ) + + def test_failed_check_stops_deploy_without_reload() -> None: """Провалившаяся проверка обрывает деплой и НЕ перезагружает Prometheus.""" branch = CHECK_FAILED_BRANCH.search(_server_script()) -- 2.45.3