"""Правки Prometheus-конфига/правил обязаны доезжать до работающего процесса. ЧТО СЛУЧИЛОСЬ НА ПРОДЕ. `GET /api/v1/status/runtimeinfo` внутри `gendesign-prometheus` 12.09 отдавал `lastConfigTime`, совпадающий со `startTime` контейнера, — конфиг и правила не перечитывались 16 суток. Деплой при этом был зелёный: `docker compose up -d` не пересоздаёт контейнер из-за изменения содержимого бинд-маунта (он сравнивает только описание сервиса), а `--web.enable-lifecycle` был включён в docker-compose.metrics.yml, но эндпоинт `/-/reload` никто не вызывал. Тот же класс бага, что уже пойман и починен для Caddy (`caddy reload`) и для Alertmanager (`--force-recreate`, см. test_3xxx_alertmanager_inode.py) в этом же workflow — только для Prometheus починка не пересоздание контейнера, а именно `POST /-/reload`: он переоткрывает файлы конфига по пути заново, так что новый инод после `git reset --hard` подхватывается без даунтайма. Проверяется здесь: (1) валидация promtool ЕСТЬ, (2) reload вызывается ТОЛЬКО после успешной валидации, (3) шаг обязан упасть, если reload не подтверждён сменой lastConfigTime. """ from __future__ import annotations 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_promtool_checks_config_and_rules() -> None: """promtool обязан проверять и конфиг, и правила — не только один файл.""" text = _text() assert "promtool check config /etc/prometheus/prometheus.yml" in text, ( "нет проверки конфига promtool'ом — битый prometheus.yml долетит до reload" ) assert "promtool check rules" in text, ( "нет проверки правил promtool'ом — битое правило долетит до reload" ) def test_reload_endpoint_is_called() -> None: """Сам reload обязан вызываться — иначе валидация ничего не решает.""" text = _text() assert "localhost:9090/-/reload" in text, ( "нет вызова POST /-/reload — конфиг/правила проверяются, но в силу не вступают " "(#3467: lastConfigTime не менялся 16 суток при зелёном деплое)" ) def test_reload_happens_after_validation_not_before() -> None: """Reload обязан идти ПОСЛЕ promtool, а не до/вместо него.""" text = _text() check_pos = text.index("promtool check config /etc/prometheus/prometheus.yml") reload_pos = text.index("localhost:9090/-/reload") assert check_pos < reload_pos, ( "reload стоит раньше проверки конфига — битый конфиг мог бы применяться вслепую" ) def test_reload_is_guarded_by_the_promtool_check() -> None: """Reload обязан быть ВНУТРИ `if promtool ...; then`, а не безусловным.""" text = _text() guard_start = text.index("if docker exec gendesign-prometheus promtool check config") else_pos = text.index("else", guard_start) reload_pos = text.index("localhost:9090/-/reload") assert guard_start < reload_pos < else_pos, ( "вызов reload лежит вне ветки успешной проверки promtool — " "битый конфиг всё равно приведёт к reload, либо reload вообще не защищён проверкой" ) def test_failed_validation_skips_reload_and_fails_the_step() -> None: """При провале promtool — reload НЕ вызывается, и шаг падает (exit 1).""" text = _text() guard_start = text.index("if docker exec gendesign-prometheus promtool check config") else_pos = text.index("else", guard_start) fi_pos = text.index("fi", else_pos) else_branch = text[else_pos:fi_pos] assert "localhost:9090/-/reload" not in else_branch, ( "reload вызывается даже в ветке провалившейся проверки" ) assert "exit 1" in else_branch, ( "провал promtool не роняет шаг — деплой останется зелёным при битом конфиге" ) def test_acceptance_checks_last_config_time_actually_changed() -> None: """Приёмка обязана сверять `lastConfigTime` до/после, а не доверять коду ответа reload. `wget` на POST /-/reload может отрапортовать успех, даже если Prometheus молча остался на старом конфиге (например, если бинарь внутри образа не поддерживает --post-data так, как ожидалось) — единственное надёжное подтверждение реального перечитывания конфига это смена таймстемпа. """ text = _text() assert text.count("lastConfigTime") >= 2, ( "нет сравнения lastConfigTime до/после — reload не проверяется по факту" ) assert "LAST_CONFIG_BEFORE" in text and "LAST_CONFIG_AFTER" in text, ( "нет явного до/после сравнения таймстемпа последней перезагрузки конфига" ) verify_start = text.index("LAST_CONFIG_AFTER") verify_block_end = text.index("Prometheus: конфиг и правила проверены", verify_start) verify_block = text[verify_start:verify_block_end] assert "exit 1" in verify_block, ( "если lastConfigTime не изменился, шаг обязан падать, а не считаться успешным" )