All checks were successful
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 / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m8s
CI / openapi-codegen-check (pull_request) Successful in 2m3s
CI / backend-tests (pull_request) Successful in 17m40s
Две дыры из deep-ревью PR #3506 — обе про «отказ выглядит как успех». M1. `stale=$(docker inspect … | while …)` под `set -eu` без `pipefail` (в POSIX-sh его нет) отдаёт статус `while`, то есть всегда 0. Провал `docker inspect` или пустой вывод давали пустой список → ветка «всё доехало» → `caddy reload` → зелёная джоба с надписью «окна недоступности нет» при прокси, работающем по СТАРОМУ конфигу. Ровно тот беззвучный отказ, ради которого написан скрипт. Теперь список читается отдельной командой, провал и пустой вывод считаются расхождением (fail-safe в прежнее поведение), число сверенных файлов печатается — «сверили пять» и «сверили ноль» в логе больше не выглядят одинаково. Ноль пофайловых маунтов (например, если Caddyfile переведут на именованный том) — тоже расхождение, а не тавтологически успешная сверка. M2. В фильтре `backend` (ci.yml) не было `ops/**`, а все содержательные регрессии живут в самом ops/caddy-apply.sh: гейт его ИСПОЛНЯЕТ. PR, правящий только скрипт, давал backend=false — джоба пропускается, гейт не исполняется, «пересоздавать всегда» уезжает в main зелёным. Тот же класс, что уже осуждён комментариями рядом (#2950/#3448/#3467). Мелочи оттуда же: * `[ -d "$src" ] && continue` вместо `[ -f "$src" ] || continue` — пропуск по `-f` склеивал «это каталог» (пропустить верно) и «файла на хосте нет», для которого в контейнере как раз живёт старый инод; * сообщение об отказе `caddy validate` больше не называет причиной битый конфиг, когда упасть мог и сам запуск проверочного контейнера; * в комментарии к проверке записана её граница: в полном деплое общий `up -d $UP_SERVICES` (deploy.yml:959) поднимает и caddy за ~110 строк до вызова скрипта, поэтому правка, которая одновременно ломает Caddyfile и меняет блок caddy в compose, пересоздаст контейнер раньше проверки. Гейт дорос с 16 до 22 проверок: `docker inspect` не ответил → пересоздание, ноль пофайловых маунтов → пересоздание, исчезнувший файл на хосте → пересоздание, число сверенных маунтов печатается, `caddy reload`/вызов скрипта не проглочены `|| true`, ci.yml-фильтр покрывает ops/**. Все 11 мутантов (7 новых + 4 прежних) краснеют, контроль зелёный. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
11 KiB
Bash
Executable file
138 lines
11 KiB
Bash
Executable file
#!/bin/sh
|
||
# Применить текущий конфиг прокси к работающему Caddy (#3443).
|
||
#
|
||
# ЗАЧЕМ. Полный деплой ПТИЦЫ пересоздавал сам Caddy БЕЗУСЛОВНО
|
||
# (`up -d --force-recreate --no-deps caddy`), а вместе с контейнером исчезал
|
||
# единственный процесс, слушающий 80/443. Замер 05.09 (#3274): 67 с `code=000`
|
||
# на ВСЕХ доменах хоста — gendsgn.ru, meraocenka.ru и зеркала, включая
|
||
# публичный лендинг МЕРЫ. Это не 502/503: принимающего процесса нет вовсе,
|
||
# поэтому заглушка окна деплоя (caddy/sites/deploy-window.caddy.snippet) здесь
|
||
# бессильна по построению — её отдаёт тот же Caddy.
|
||
#
|
||
# ЧТО НА САМОМ ДЕЛЕ ТРЕБУЕТ ПЕРЕСОЗДАНИЯ. Безусловный флаг появился 17.05
|
||
# (11e78d73) ради нового bind-маунта `./preview` из docker-compose.prod.yml,
|
||
# который «не появлялся в running container». Довод неверен: `docker compose
|
||
# up -d` БЕЗ `--force-recreate` пересоздаёт контейнер сам, как только меняется
|
||
# описание сервиса или образ. Проверено на живом демоне (docker 28.4):
|
||
# добавлен volume → `Container … Starting/Started`, id контейнера новый;
|
||
# тег указан на др. образ → id новый;
|
||
# не менялось ничего → `Container … Running`, id тот же.
|
||
#
|
||
# Остаётся ровно один класс изменений, которого compose не видит: СОДЕРЖИМОЕ
|
||
# пофайлового bind-маунта. `git reset --hard` не правит файл на месте, а пишет
|
||
# новый инод; контейнер держит примонтированным прежний и продолжает читать
|
||
# его — `caddy reload` перечитает ровно тот же старый инод. Тот же механизм уже
|
||
# ловили на Alertmanager (27.08, deploy-metrics.yml) и на Alloy (#3380). У Caddy
|
||
# так смонтированы пять путей: Caddyfile и четыре сниппета. Каталоги
|
||
# (caddy/sites, caddy/local, preview) этим не страдают — правка внутри каталога
|
||
# видна контейнеру сразу, поэтому самый частый случай (caddy/sites/apps.caddy)
|
||
# пересоздания НЕ требует.
|
||
#
|
||
# ОТСЮДА ПОРЯДОК: проверить конфиг → `up -d` без `--force-recreate` → если
|
||
# контейнер остался тем же, сверить, видит ли он текущее содержимое пофайловых
|
||
# маунтов → пересоздать ТОЛЬКО при расхождении, иначе `caddy reload`, который
|
||
# не рвёт соединения.
|
||
#
|
||
# ГРАНИЦА. Сверка по СОДЕРЖИМОМУ, а не по иноду: файл, переписанный тем же
|
||
# текстом, пересоздания не требует. Не прочиталось (контейнер не запущен, в
|
||
# образе нет sha256sum) — считаем расхождением: fail-safe в сторону прежнего
|
||
# поведения, то есть пересоздания.
|
||
set -eu
|
||
|
||
# Оба вызывающих (job `deploy` и job `deploy-caddy` в .forgejo/workflows/deploy.yml)
|
||
# работают в /opt/gendesign, но не зависеть от cwd дешевле, чем это помнить.
|
||
cd "$(dirname "$0")/.."
|
||
|
||
COMPOSE="docker compose -p gendesign -f docker-compose.prod.yml"
|
||
|
||
caddy_cid() { $COMPOSE ps -aq caddy 2>/dev/null | tail -n1; }
|
||
|
||
# ── 1. Проверка ДО применения ────────────────────────────────────────────────
|
||
# Одноразовый контейнер читает файлы С ХОСТА — то есть ровно то, что поедет в
|
||
# работающий Caddy. `exec caddy validate` здесь не годится: он проверил бы
|
||
# старый инод, то есть предыдущую версию конфига. Образ и парсер те же, что на
|
||
# PR-гейте (ci.yml «Guard: Caddyfile синтаксически валиден»).
|
||
#
|
||
# ГРАНИЦА ЭТОЙ ПРОВЕРКИ. Она обрывает применение до того, как конфиг попадёт в
|
||
# работающий Caddy, — но только на быстром пути. В полном деплое ВЫШЕ по
|
||
# скрипту (deploy.yml, `up -d $UP_SERVICES`) уже прошёл общий подъём всех
|
||
# сервисов, и если правка одновременно ломает Caddyfile И меняет блок caddy в
|
||
# docker-compose.prod.yml, контейнер пересоздастся там — с непроверенным
|
||
# конфигом и раньше этой строки. Первая линия против этого — гейт на PR (#2913).
|
||
echo "→ проверяю конфиг прокси одноразовым контейнером…"
|
||
if ! docker run --rm -v "$PWD:/work:ro" -w /work caddy:2 \
|
||
caddy validate --config /work/Caddyfile --adapter caddyfile; then
|
||
echo "ОШИБКА: конфиг прокси не применён — проверка не пройдена ЛИБО не удалось"
|
||
echo " запустить проверочный контейнер (нет образа caddy:2, занят демон,"
|
||
echo " недоступен реестр). Причина — в выводе выше, не гадать по этой строке."
|
||
echo " Работающий Caddy не тронут, домены живы."
|
||
exit 1
|
||
fi
|
||
|
||
# ── 2. Описание сервиса и образ ──────────────────────────────────────────────
|
||
before=$(caddy_cid)
|
||
$COMPOSE up -d --no-deps caddy
|
||
after=$(caddy_cid)
|
||
if [ -z "$after" ]; then
|
||
echo 'ОШИБКА: после `up -d` контейнера caddy нет — смотри вывод compose выше.'
|
||
exit 1
|
||
fi
|
||
if [ "$before" != "$after" ]; then
|
||
echo "✓ Caddy пересоздан compose'ом: изменилось описание сервиса или образ (${before:-нет}→${after})."
|
||
exit 0
|
||
fi
|
||
|
||
# ── 3. Доехало ли содержимое пофайловых маунтов ──────────────────────────────
|
||
# Список маунтов читается ОТДЕЛЬНОЙ командой, а не в конвейере с циклом: в
|
||
# `$(docker inspect … | while …)` статус подстановки — это статус `while`, то
|
||
# есть всегда 0 (`pipefail` в POSIX-sh нет вовсе). Провал `docker inspect`
|
||
# давал бы пустой список → «расхождений нет» → `caddy reload` → зелёная джоба с
|
||
# надписью «окна недоступности нет», а прокси работал бы по СТАРОМУ конфигу.
|
||
# Это ровно тот беззвучный отказ, ради которого написан весь скрипт.
|
||
mounts=$(docker inspect "$after" \
|
||
--format '{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}}|{{.Destination}}{{println}}{{end}}{{end}}') \
|
||
|| mounts=''
|
||
|
||
if [ -z "$mounts" ]; then
|
||
echo "WARNING: список маунтов Caddy не прочитан (docker inspect молчит или упал) —"
|
||
echo " сверить нечем, считаю расхождением: fail-safe в прежнее поведение."
|
||
verdicts=""
|
||
stale="(маунты не прочитаны)"
|
||
else
|
||
verdicts=$(printf '%s\n' "$mounts" | while IFS='|' read -r src dst; do
|
||
[ -n "${dst:-}" ] || continue
|
||
# Каталог инод не держит — пропускаем. Именно `-d`, а не `-f`:
|
||
# отсутствующий/нечитаемый ФАЙЛ — не повод молча пропустить, в
|
||
# контейнере в этот момент живёт старый инод со старым текстом.
|
||
if [ -d "$src" ]; then continue; fi
|
||
host_sum=$(sha256sum "$src" 2>/dev/null | cut -d' ' -f1)
|
||
seen_sum=$(docker exec "$after" sha256sum "$dst" 2>/dev/null | cut -d' ' -f1)
|
||
if [ -n "$host_sum" ] && [ "$host_sum" = "${seen_sum:-НЕ_ПРОЧИТАНО}" ]; then
|
||
echo "OK $dst"
|
||
else
|
||
echo "STALE $dst"
|
||
fi
|
||
done)
|
||
# «Сверили пять файлов» и «сверили ноль» обязаны различаться в логе — иначе
|
||
# зелёная строка ниже одинаково означает и проверку, и её отсутствие.
|
||
checked=$(printf '%s\n' "$verdicts" | grep -c . || true)
|
||
echo "→ сверено пофайловых маунтов с тем, что видит контейнер: $checked"
|
||
stale=$(printf '%s\n' "$verdicts" | sed -n 's/^STALE //p' | tr '\n' ' ')
|
||
if [ "$checked" -eq 0 ]; then
|
||
echo "WARNING: ни одного пофайлового bind-маунта не найдено — у Caddy их пять"
|
||
echo " (Caddyfile + 4 сниппета). Считаю расхождением: fail-safe."
|
||
stale="(пофайловых маунтов не найдено)"
|
||
fi
|
||
fi
|
||
|
||
if [ -n "$stale" ]; then
|
||
echo "→ до контейнера НЕ доехали пофайловые маунты: $stale"
|
||
echo " (bind-маунт файла держит инод: reload перечитал бы старую версию — нужен recreate)"
|
||
$COMPOSE up -d --force-recreate --no-deps caddy
|
||
echo "✓ Caddy пересоздан — иначе правка осталась бы неприменённой."
|
||
exit 0
|
||
fi
|
||
|
||
# ── 4. Всё доехало — перезагрузка без разрыва соединений ─────────────────────
|
||
$COMPOSE exec -T caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||
echo "✓ конфиг прокси перезагружен без пересоздания контейнера — окна недоступности нет."
|