# ТЗ: Healthcheck enduro-trails-app — заменить curl на python-проверку

**Work Item:** ET-015
**Базовый документ:** [01-brd.md](01-brd.md)
**Версия:** 1.0
**Дата:** 2026-06-05

## 1. Постановка

Заменить в `docker-compose.yml` (сервис `app`) healthcheck-команду
так, чтобы она:
- использовала средства, **уже доступные** в образе `python:3.12-slim`
  (т.е. интерпретатор `python3`), без установки дополнительных пакетов;
- обращалась к `http://localhost:5556/api/health` и трактовала
  HTTP-код **2xx** как «healthy», любой иной отклик и любую ошибку
  соединения — как «unhealthy»;
- укладывалась в `timeout: 5s`.

## 2. Текущее состояние

`docker-compose.yml`, строки 22–26:

```yaml
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:5556/api/health"]
  interval: 30s
  timeout: 5s
  retries: 3
```

`Dockerfile`: базовый образ `python:3.12-slim`. `curl` отсутствует.
Установлен `pip`, доступен `python3` (и алиас `python`).

`src/api/main.py:1224`:

```python
@app.get("/api/health")
async def health():
    return {
        "status": "ok",
        "db_path": DATA_PATH,
        "db_exists": os.path.exists(DATA_PATH),
    }
```

Возвращает HTTP 200 + JSON. Менять не требуется.

## 3. Целевое состояние

### 3.1. Изменение в `docker-compose.yml`

Секция `healthcheck` сервиса `app` приводится к виду:

```yaml
healthcheck:
  test:
    - "CMD"
    - "python"
    - "-c"
    - "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:5556/api/health', timeout=3).status == 200 else 1)"
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 20s
```

Пояснения:
- `CMD` (а не `CMD-SHELL`) — никакого shell-парсинга, аргументы передаются
  как массив, экранирование не нужно.
- `python` — алиас, имеющийся в `python:3.12-slim` (есть и `python3`,
  оба указывают на один интерпретатор).
- `urllib.request.urlopen(..., timeout=3)` — стандартная библиотека,
  без зависимостей; внутренний `timeout=3` короче внешнего
  `timeout: 5s`, остаётся запас на старт интерпретатора.
- `sys.exit(0 if ... == 200 else 1)` — корректное преобразование
  статуса HTTP в exit code. Любой raise (URLError, HTTPError, timeout)
  пробросится наверх, процесс завершится ненулевым кодом → `unhealthy`.
- `start_period: 20s` — добавляется, чтобы Docker не считал ранние
  ошибки запуска приложения «провалом» healthcheck в окне старта.
  Uvicorn поднимается за < 2 c, 20 с — комфортный запас.

### 3.2. Изменения в `Dockerfile`

**Не требуются.** Добавлять `curl` через `apt-get` нельзя — раздувает
образ и противоречит выбранному подходу.

### 3.3. Изменения в `src/api/main.py`

**Не требуются.** Эндпоинт `/api/health` существует и отдаёт 200.

## 4. Альтернативы (рассмотрены и отклонены)

| Вариант | Плюсы | Минусы | Решение |
|---------|-------|--------|---------|
| `apt-get install curl` в Dockerfile | Привычная команда | +~10 МБ к образу, новый APT-слой, противоречит slim-философии | Отклонено |
| `wget --spider` | Однострочник | `wget` тоже отсутствует в `python:3.12-slim` (проверено: пакет `wget` не входит в slim) | Отклонено |
| HEALTHCHECK в Dockerfile | Декларативно | Дублирует compose, при изменении нужно пересобирать образ | Отклонено, держим в compose |
| Отдельный health-скрипт `scripts/healthcheck.py` | Чище YAML | Лишний файл для одной строки, мутит образ | Отклонено |

Принятый вариант: **inline python one-liner** через `urllib.request`.

## 5. Реализационные требования

### R-1. Изменение `docker-compose.yml`
- В сервисе `app` секция `healthcheck` заменяется на конструкцию из
  п. 3.1.
- Остальные параметры сервиса (ports, volumes, environment) не
  затрагиваются.

### R-2. Идемпотентность пересборки
- Изменения не требуют ребилда образа (`docker compose build`).
  Достаточно `docker compose up -d app` для пересоздания контейнера
  с новой healthcheck-командой.
- Допускается ребилд при необходимости — это не должно ломать сборку.

### R-3. Обратная совместимость
- Никаких ENV-переменных, влияющих на путь healthcheck, не вводится.
  Адрес `http://localhost:5556/api/health` зашит в строку.
  (Локальный — `localhost` внутри контейнера; порт всегда 5556,
   как ENV `PORT` в Dockerfile.)

### R-4. Документация
- В `docs/work-items/ET-015/06-adr/healthcheck-via-python.md` зафиксировать
  решение «использовать python-one-liner вместо curl». Автор ADR —
  следующий этап (Architecture), не Анализ.
- Обновить `CHANGELOG.md` в секции «Unreleased» строкой формата
  `fix(infra): use python urllib for container healthcheck (ET-015)`.

### R-5. Линт и форматирование
- YAML-валидность `docker-compose.yml` проверяется `make lint`.
- Длина строки python one-liner допустима в YAML (нет лимита 120 для
  строковых значений multi-line array).

## 6. Тестирование

См. [04-test-plan.yaml](04-test-plan.yaml). Кратко:

- **integration-1**: после `docker compose up -d app` контейнер должен
  выйти в `healthy` за ≤ 120 с.
- **integration-2**: при остановке uvicorn (или искусственном блоке
  порта) — за ≤ 120 с переходит в `unhealthy`.
- **unit-1** (опционально): smoke-тест python-one-liner вне Docker
  через `python -c "..."` против поднятого локально `make dev`.

## 7. Деплой и откат

- Деплой: `make deploy-test` (как обычно). При деплое compose
  пересоздаст контейнер `enduro-trails-app-1`.
- Проверка: `docker inspect enduro-trails-app-1 --format
  '{{.State.Health.Status}}'` → `healthy` в течение нескольких циклов
  (`interval=30s × 3 = 90s` плюс `start_period=20s`).
- Откат: `git revert` коммита; `docker compose up -d app`. Старая
  (поломанная) healthcheck-команда вернётся, но сам сервис продолжит
  работать.

## 8. Риски

| Риск | Вероятность | Митигация |
|------|-------------|-----------|
| Python one-liner крэшится на каком-то Docker-движке из-за квотинга | низкая | YAML-массив `["CMD", "python", "-c", "..."]` — без shell, без экранирования |
| Длинная строка усложняет редактирование | средняя | Использовать YAML block-scalar (`>-` или `|`) при необходимости, но в текущей форме строка читаемая |
| Эндпоинт `/api/health` в будущем сделают «дорогим» и timeout=3s не хватит | низкая | Эндпоинт сейчас отдаёт ~7 мс; при изменении — пересмотр timeout |
| На prod-среде iptables/сеть отличаются и localhost внутри контейнера ведёт себя иначе | очень низкая | `localhost` в network namespace контейнера = loopback контейнера, не зависит от хоста |

## 9. Definition of Ready (для следующего этапа)

- BRD прочитан, ТЗ согласовано.
- Доступ к тестовой среде mva154 для проверки `docker inspect`.
- `make deploy-test` и `docker compose` доступны из ветки.
