All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 1m59s
CI / backend-tests (pull_request) Successful in 17m26s
Две вещи, вторая — исправление собственной ошибки из #3127. ## Дежурного зовут по имени 27.08 продукты лежали 10 часов, и в канале «диск занят на 86 %» и «клиенты не могут открыть сайт» выглядели одинаково. Появился отдельный маршрут: severity=critical И host=apps, то есть критично на ПРОДУКТОВОЙ машине — значит людям недоступна МЕРА и Site Finder, а не «где-то в инфраструктуре тесно». У такого сообщения другой текст (🚨 КЛИЕНТЫ ЗАТРОНУТЫ), упоминание дежурного и repeat_interval 30 минут против 3 часов у прочего критичного: пока инцидент не погашен, напоминание должно быть неудобным. Сужение по host=apps существенно. Без него дежурного звали бы на каждую инфраструктурную мелочь, и тег перестал бы что-либо значить за неделю. Сам аккаунт в репозиторий не попадает — берётся из METRICS_TELEGRAM_ONCALL. Дежурный меняется, конфиг в git — нет. Пустая переменная = сообщение без тега, поведение не ломается. ## Починка: комментарий внутри продолжения команды В #3127 блок rm -f вместе с комментарием встал МЕЖДУ строками, каждая из которых заканчивалась обратным слешем. Строки склеиваются, и весь вызов envsubst уехал в комментарий — конфиг Alertmanager перестал бы рендериться вовсе, при полностью зелёном деплое. Синтаксически это корректный шелл, bash -n такое не ловит, а в диффе не видно: строки выглядят как отдельные. Поэтому проверка структурная и применяется ко ВСЕМ shell-блокам workflow, а не только к месту ожога.
123 lines
7.2 KiB
Python
123 lines
7.2 KiB
Python
r"""Клиентский инцидент зовёт дежурного поимённо — и шелл в workflow не разваливается (#3078).
|
||
|
||
ДВА ИНВАРИАНТА, и второй появился из-за собственной ошибки.
|
||
|
||
1. Маршрут для клиентских инцидентов. 27.08 продукты лежали 10 часов, и в канале
|
||
«диск занят на 86 %» и «клиенты не могут открыть сайт» выглядели одинаково.
|
||
Отдельный приёмник `telegram-clients` с упоминанием дежурного и коротким
|
||
`repeat_interval` — это и есть разница между «шумит» и «зовёт».
|
||
|
||
2. Комментарий не должен стоять между строками продолжения команды. В #3127 блок
|
||
`rm -f` вместе с комментарием встал МЕЖДУ строками, каждая из которых
|
||
заканчивалась обратным слешем:
|
||
|
||
VAR1="..." \
|
||
VAR2="..." \
|
||
# комментарий <- строки склеиваются, дальше всё уходит в комментарий
|
||
rm -f ...
|
||
envsubst ... > конфиг <- НИКОГДА не выполнялся
|
||
|
||
Синтаксически это корректный шелл, поэтому `bash -n` такое не ловит, а глазами
|
||
в диффе не видно: строки выглядят как отдельные. Результат — конфиг Alertmanager
|
||
молча перестаёт рендериться, то есть алертинг исчезает целиком при зелёном
|
||
деплое. Проверка структурная, потому что никакой линтер этого за нас не сделает.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
import yaml
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||
WORKFLOW = REPO_ROOT / ".forgejo" / "workflows" / "deploy-metrics.yml"
|
||
TEMPLATE = REPO_ROOT / "ops" / "metrics" / "alertmanager" / "alertmanager.yml.tmpl"
|
||
|
||
|
||
def _shell_blocks() -> list[str]:
|
||
"""Все shell-скрипты из workflow: и `run:`, и `script:` у ssh-action."""
|
||
doc = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
|
||
blocks: list[str] = []
|
||
for job in (doc.get("jobs") or {}).values():
|
||
for step in job.get("steps") or []:
|
||
if isinstance(step.get("run"), str):
|
||
blocks.append(step["run"])
|
||
with_ = step.get("with") or {}
|
||
if isinstance(with_.get("script"), str):
|
||
blocks.append(with_["script"])
|
||
assert blocks, "в workflow не нашлось ни одного shell-блока — тест ослеп"
|
||
return blocks
|
||
|
||
|
||
def test_kommentariy_ne_stoit_vnutri_prodolzheniya_komandy() -> None:
|
||
"""После строки с продолжением не может идти комментарий.
|
||
|
||
Ровно эта ошибка увела вызов envsubst в комментарий и оставила Alertmanager
|
||
без конфига. Проверка применяется ко ВСЕМ shell-блокам workflow, а не только
|
||
к тому месту, где обожглись.
|
||
"""
|
||
for block in _shell_blocks():
|
||
lines = block.split("\n")
|
||
for i, line in enumerate(lines[:-1]):
|
||
if not line.rstrip().endswith("\\"):
|
||
continue
|
||
nxt = lines[i + 1].strip()
|
||
assert not nxt.startswith("#"), (
|
||
"комментарий внутри продолжения команды — всё, что ниже, "
|
||
f"уедет в комментарий:\n {line.strip()}\n {nxt}"
|
||
)
|
||
|
||
|
||
def test_priyomnik_klientskih_incidentov_est() -> None:
|
||
"""В шаблоне есть отдельный приёмник и маршрут на него."""
|
||
text = TEMPLATE.read_text(encoding="utf-8")
|
||
assert "- name: telegram-clients" in text, "пропал приёмник клиентских инцидентов"
|
||
assert "receiver: telegram-clients" in text, "на приёмник никто не маршрутизирует"
|
||
|
||
|
||
def test_marshrut_klientov_ranshe_obschego_critical() -> None:
|
||
"""Клиентский маршрут должен стоять ВЫШЕ общего `severity=critical`.
|
||
|
||
Alertmanager берёт первый подошедший маршрут. Если общий окажется выше,
|
||
клиентские инциденты уйдут в него и упоминание не сработает — отказ тихий:
|
||
сообщения приходят, просто без тега.
|
||
"""
|
||
text = TEMPLATE.read_text(encoding="utf-8")
|
||
i_clients = text.index("receiver: telegram-clients")
|
||
i_generic = text.index("# Прочее критичное")
|
||
assert i_clients < i_generic, "общий critical перехватит клиентские инциденты раньше"
|
||
|
||
|
||
def test_upominanie_iz_peremennoy_a_ne_zashito() -> None:
|
||
"""Дежурный задаётся переменной: он меняется, а конфиг в git — нет."""
|
||
text = TEMPLATE.read_text(encoding="utf-8")
|
||
assert "${METRICS_TELEGRAM_ONCALL}" in text, "упоминание дежурного не параметризовано"
|
||
assert "@leks361" not in text, "конкретный аккаунт зашит в репозиторий"
|
||
|
||
|
||
def test_peremennaya_dezhurnogo_dohodit_do_shablona() -> None:
|
||
"""Переменная реально прокидывается и подставляется, а не объявлена вхолостую.
|
||
|
||
Три звена, и каждое рвётся молча: секрет → env шага → список envsubst. Без
|
||
последнего `${METRICS_TELEGRAM_ONCALL}` останется в конфиге буквально.
|
||
"""
|
||
wf = WORKFLOW.read_text(encoding="utf-8")
|
||
assert "METRICS_TELEGRAM_ONCALL: ${{ secrets.METRICS_TELEGRAM_ONCALL }}" in wf
|
||
m = re.search(r"envs:\s*(\S+)", wf)
|
||
assert m and "METRICS_TELEGRAM_ONCALL" in m.group(1), "переменная не форвардится в ssh-шаг"
|
||
m2 = re.search(r"envsubst '([^']+)'", wf)
|
||
assert m2 and "${METRICS_TELEGRAM_ONCALL}" in m2.group(1), "переменной нет в списке envsubst"
|
||
|
||
|
||
@pytest.mark.parametrize("marker", ['severity = "critical"', 'host = "apps"'])
|
||
def test_klientskiy_marshrut_suzhen_po_oboim_priznakam(marker: str) -> None:
|
||
"""Маршрут ловит именно критичное НА ПРОДУКТОВОМ хосте.
|
||
|
||
Без `host = "apps"` дежурного звали бы на каждую инфраструктурную мелочь, и
|
||
тег быстро перестал бы что-либо значить.
|
||
"""
|
||
text = TEMPLATE.read_text(encoding="utf-8")
|
||
block = text[text.index("receiver: telegram-clients") : text.index("# Прочее критичное")]
|
||
assert marker in block, f"в клиентском маршруте нет условия {marker}"
|