All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 3m14s
CI / backend-tests (pull_request) Successful in 19m15s
lastConfigTime у gendesign-prometheus совпадал со startTime контейнера 16 суток: docker compose up -d не пересоздаёт контейнер из-за изменения содержимого бинд-маунта (сравнивается только описание сервиса), а --web.enable-lifecycle был включён, но /-/reload никто не вызывал. Любая правка ops/metrics/prometheus/** доезжала до диска и молча не вступала в силу до случайного рестарта, при зелёном деплое. Добавлен шаг по образцу уже работающей проверки Caddyfile в этом же workflow: promtool check config + promtool check rules внутри контейнера, reload только при успешной проверке, приёмка через сравнение lastConfigTime до/после (обновляется на каждый успешный reload, поэтому надёжно ловит и несостоявшийся вызов). Провал promtool теперь роняет шаг и не трогает работающий Prometheus. Alertmanager уже чинился отдельно (--force-recreate, #3078/#3136) — reload для него намеренно не помогает из-за переиспользуемого инода, это не regressed. Loki (/etc/loki/loki-config.yml), Grafana (provisioning) и Alloy (config.alloy) в том же деплое лежат на дисковых бинд-маунтах без reload — чинить их этим PR не стал, см. summary задачи. Refs #3467, #3471
112 lines
6.4 KiB
Python
112 lines
6.4 KiB
Python
"""Правки 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 не изменился, шаг обязан падать, а не считаться успешным"
|
||
)
|