e6ba503c20
Refs #21, Refs #56
210 lines
16 KiB
Markdown
210 lines
16 KiB
Markdown
# Проверка dev/staging стенда (#21) — фактические прогоны на Docker-хосте
|
||
|
||
Дата: 2026-10-22 · Хост: `192.168.1.147` (Debian 13, Docker 29.4.1, compose v5.1.3, kernel 6.17)
|
||
Ветка: `feat/21-dev-stand`, HEAD `d63b9cea` · Ref: #21, #15, #17, #18, #31, #56
|
||
|
||
## 0. Стенд запускает именно то, что в ветке
|
||
|
||
Каталог стенда `(/opt/awg-dash-dev/repo)` не является git-чекаутом (`git clone` по HTTP обрывается
|
||
на этом Gitea — `fetch-pack: invalid index-pack output`), поэтому файлы сверены побайтово
|
||
через git-blob-SHA с деревом HEAD:
|
||
|
||
| файл | git blob (HEAD == хост) |
|
||
|---|---|
|
||
| `Makefile` | `638ec381f3d1e57a8ab84f194e40ff6a0c51d8cf` |
|
||
| `deploy/docker-compose.dev.yml` | `6bed89a5ea2a197aa9f6fd6bcda6d8131303114d` |
|
||
| `deploy/docker-compose.stub.yml` | `936efb38dfff4ba8b942c9757acd4ba9c39c4064` |
|
||
| `deploy/docker-compose.test.yml` | `b628c129ae4de9b33875503b2c8273675b235a84` |
|
||
| `deploy/verify-dev-stand.sh` | `c4e13f11f9023700be531ebb312ad33d7beea4d8` |
|
||
| `deploy/api_stub.py` | `93663113f118676a7e896e0e9d32bd20a21f6a0e` |
|
||
| `tests/deploy/validate_compose.py` | `0fa8b3c02675a364f4f5b447220680a7818faa5a` |
|
||
| `tests/deploy/pytest.ini` | `7299b6b45487dd4f214f0444e2f0f5c237eb2fac` |
|
||
| `tests/deploy/.coveragerc` | `9b41183e29a155b5498c3722cf4639e9c69268c6` |
|
||
| `tests/deploy/test_api_stub.py` | `1d5cec3a72b02da6cb48a6ca433e7699b768f631` |
|
||
| `tests/deploy/test_validate_compose.py` | `fb045949848cb70a1880614201930c2d4ecd9d0a` |
|
||
| `tests/netns/lib.sh` | `806375299b7c3932d20b275d98b72f65e48940ec` |
|
||
| `tests/netns/run.sh` | `c6797bf9c42abe7c4f72cf5402af82e2192ee867` |
|
||
| `tests/netns/topology.sh` | `4892f6b9f9b92c38ae058df00e7c79a692537eaa` |
|
||
| `tests/netns/netns_common.py` | `9d3bfd94cd8e8b321af92274835b3f0ebc48c842` |
|
||
| `tests/netns/README.md` | `845863188ffb26c1aae7e475addc0c511e0d8f1f` |
|
||
| остальные файлы `tests/netns/` (5) | без изменений с раунда 1 |
|
||
|
||
## 1. Предварительные пакеты на стенд-хосте
|
||
|
||
```bash
|
||
apt-get install -y make python3-yaml python3-pytest python3-pytest-cov shellcheck
|
||
```
|
||
|
||
(`make` на хосте отсутствовал — без него гейты `make *` невоспроизводимы; пакеты добавлены
|
||
сверх существующего набора, ничего не удалялось и не переконфигурировалось.)
|
||
|
||
## 2. Гейты (реальные прогоны)
|
||
|
||
Лог целиком: `/opt/awg-dash-dev/verify-final-1792617226.log` (на хосте).
|
||
|
||
| гейт | команда | результат |
|
||
|---|---|---|
|
||
| манифесты | `make test-manifests` | exit 0, `validate-compose: ok (1 предупреждений)`¹ |
|
||
| статический анализ шелла | `make lint-shell` | exit 0 |
|
||
| unit-тесты манифестов | `python3 -m pytest tests/deploy -q --no-cov` | exit 0 |
|
||
| netns-харнесс (AC-3) | `make test-netns` | exit 0, `8 ok / 0 fail / 0 err`, отчёт `/tmp/awg-netns/report.json` |
|
||
| AC-1 (health 200) | `make dev-up` + `curl :8180/api/v1/health` | HTTP 200, контейнер `healthy` |
|
||
| AC-2 (состояние переживает рестарт) | `make dev-verify` | `[ ok ] AC-1`, `[ ok ] AC-2` ×2, exit 0 |
|
||
| логи | `make dev-logs` | стрим логов, есть `[api-stub] ... GET /api/v1/health 200` |
|
||
|
||
¹ предупреждение: `build.dockerfile deploy/Dockerfile отсутствует в дереве (ожидаемо до мержа #18)`.
|
||
Образ `awg-dash:dev` для стенда собран из Dockerfile PR #49 (ветка `feat/18-docker-runtime`), в дерево
|
||
этой ветки он не входит: `make build` вернёт внятную ошибку до мержа #18. Валидатор переключается в
|
||
режим ошибки флагом `make test-manifests MANIFEST_ARGS=--require-dockerfile` — этот режим нужно включить
|
||
в CI гейте после мержа #18.
|
||
|
||
### AC-1 — детали
|
||
|
||
```
|
||
$ make dev-up
|
||
# (авто-режим: backend/awg_api/app.py отсутствует (#31) → + deploy/docker-compose.stub.yml)
|
||
$ curl -s -w '\nHTTP=%{http_code}\n' http://127.0.0.1:8180/api/v1/health
|
||
{"issue": "#31", "note": "заглушка dev-стенда: реальный API появится в #31", "status": "ok",
|
||
"stub": true, "stub_env": "1", "uptime_s": 14.909}
|
||
HTTP=200
|
||
$ docker ps --filter name=awg-dash-dev
|
||
awg-dash-dev Up 15 seconds (healthy) 0.0.0.0:8180->8080/tcp, [::]:8180->8080/tcp
|
||
```
|
||
|
||
Почему с заглушкой: FastAPI-каркас (#31) ещё не реализован, сам образ HTTP не поднимает (entrypoint
|
||
пишет `awg_api/app.py ещё не реализован` и ждёт). Чтобы AC-1 #21 проверялся на живом compose-стенде
|
||
уже в Sprint 0, `make dev-up` добавляет оверрайд `docker-compose.stub.yml` **только пока
|
||
`backend/awg_api/app.py` отсутствует**; ответ явно помечен `"stub": true`. После мержа #31 оверрайд
|
||
не добавляется, `make dev-up` поднимает реальный API без правок Makefile.
|
||
|
||
### AC-2 — детали
|
||
|
||
```
|
||
$ make dev-verify
|
||
[verify][ ok ] AC-1: http://127.0.0.1:8180/api/v1/health -> 200 (api=stub)
|
||
[verify] AC-2: пишу маркеры в /config и /data и перезапускаю контейнер
|
||
[verify][ ok ] AC-2: /config и /data пережили restart и пересоздание контейнера
|
||
[verify][ ok ] AC-2: маркеры видны на хосте (/opt/awg-dash-dev/{config,data}) — это bind mount, а не слой контейнера
|
||
|
||
$ ls /opt/awg-dash-dev/config /opt/awg-dash-dev/data
|
||
/opt/awg-dash-dev/config/.ac2-marker (24 байта)
|
||
/opt/awg-dash-dev/data/.ac2-marker (24 байта)
|
||
```
|
||
|
||
Проверяется два сценария: `docker compose restart` (как в AC) и `up -d --force-recreate` (сильнее).
|
||
|
||
### AC-3 (netns) — детали
|
||
|
||
```
|
||
[netns] создаю netns: client=awgt21-client gw=awgt21-gw uplink=awgt21-uplink
|
||
[ ok ] стенд поднят: awgt21-client(10.213.0.2) -> awgt21-gw(10.213.0.1/10.214.0.1) -> awgt21-uplink(10.214.0.2)
|
||
[ ok ] policy routing: fwmark 51820 -> table 51820 (default via 10.214.0.2)
|
||
[ ok ] mock endpoint: udp/51820 в awgt21-uplink; детектор утечки: 198.51.100.9:9999/udp
|
||
Ran 8 tests in 5.447s — OK
|
||
test_01_namespaces_and_links ... test_08_killswitch_blocks_direct_egress — все ok
|
||
[ ok ] топология снесена, namespace'ы удалены (ip netns list пусто)
|
||
```
|
||
|
||
`report.json`: `{"suite": "tests/netns", "tests": 8, "passed": 8, "failures": 0, "errors": 0,
|
||
"skipped": 0, "returncode": 0, "duration_s": 5.447}`
|
||
|
||
## 3. Что чинилось в этой итерации
|
||
|
||
| проблема | причина | правка |
|
||
|---|---|---|
|
||
| `make dev-up` давал `unhealthy` и AC-1 не выполнялся | API (#31) не реализован | `Makefile`: авто-подключение stub-оверрайда, пока нет `backend/awg_api/app.py`; `dev-up-app` / `dev-up-stub` для явных режимов |
|
||
| `make dev-verify` печатал `[verify][fail] нет монтирования /config` при реально сохранённых томах и падал | `verify-dev-stand.sh` кэшировал id контейнера и делал `docker inspect` по нему после `--force-recreate` → `no such object` | перезапрос id (`cid()`) после пересоздания и явный `fatal_env` вместо ложного fail |
|
||
| `dev-up` падал без `deploy/Dockerfile` (`--build`) | Dockerfile живёт в #18, ветка не смёржена | `--build` форсируется только при наличии `deploy/Dockerfile` |
|
||
|
||
## 4. Ограничения и что делать дальше
|
||
|
||
1. **CI-гейты (DoD) не автоматизированы:** Gitea Actions runner для `embeddevio/awg-dash` не зарегистрирован
|
||
(см. отчёт в #17, run #1 остаётся `queued`). Все гейты в таблице §2 прогнаны **вручную** на стенд-хосте;
|
||
после регистрации runner'а в #17 они переносятся в workflow без изменений команд.
|
||
2. **Порт:** на `192.168.1.147` хост-порт 8080 занят (`drawio`), поэтому стенд публикуется как `8180:8080`
|
||
(контейнерный порт приложения — 8080, `AWG_DASH_PORT` переопределяет хост-порт).
|
||
3. **Сборка образа** — только после мержа #18; до этого образ `awg-dash:dev` собирается из ветки #18 вручную.
|
||
4. **Prod/staging:** stub-оверрайд и `deploy/api_stub.py` — DEV-ONLY. В staging/prod поднимается только
|
||
`docker-compose.dev.yml` (или прод-манифест) без stub-файла.
|
||
|
||
## 5. Откат / очистка
|
||
|
||
```bash
|
||
make dev-down # остановить стенд, каталоги /opt/awg-dash-dev/{config,data} сохраняются
|
||
make dev-reset # + удалить осиротевшие контейнеры/сети проекта
|
||
make test-netns # netns-харнесс сносит топологию сам; аварийно: bash tests/netns/topology.sh down
|
||
```
|
||
|
||
## 6. Раунд 2 (issue #56): netns в контейнере и покрытие тестов
|
||
|
||
Дата: 2026-10-28 · Хост: `192.168.1.147` · Ветка: `feat/21-dev-stand` · Ref: #21, #15, #17, #56
|
||
|
||
Ревью qa-engineer (раунд 1) нашло два блокера: `make test-netns-ci` падал в контейнере
|
||
(`mount of /sys failed: Operation not permitted`, exit 255 вместо 0/1/3/4) и гейт покрытия
|
||
`pytest tests/deploy` обходился флагом `--no-cov` (0.00 %, `deploy/api_stub.py` 0 %).
|
||
|
||
### 6.1 Причина падения netns в контейнере
|
||
|
||
Воспроизведено на образе `awg-dash:dev` (iproute2 6.11.0):
|
||
|
||
| команда в контейнере | результат |
|
||
|---|---|
|
||
| `ip netns add <ns>` | rc=0 |
|
||
| `ip netns exec <ns> true` | `mount of /sys failed: Operation not permitted`, rc=255 |
|
||
| `ip -n <ns> link set lo up` | `mount of /sys failed: Operation not permitted`, rc=255 |
|
||
| `nsenter --net=/var/run/netns/<ns> true` | rc=0 |
|
||
|
||
`ip netns exec` и `ip -n` перемонтируют `/sys` внутри namespace; в контейнере это падает.
|
||
Поэтому харнесс больше не использует эти формы: вход в namespace идёт через `ns_exec()` /
|
||
`ns_prefix()` (`tests/netns/lib.sh`) и `netns_common.ns_sh()`, которые один раз пробуют
|
||
`ip netns exec`, а при отказе переключаются на `nsenter --net=...` (`AWG_NS_EXEC_MODE=auto|ip|nsenter`).
|
||
Все `ip -n "$NS"` заменены на `ns_exec "$NS" ip ...`.
|
||
|
||
Коды возврата приведены к контракту `0/1/3/4` (`run.sh`, `topology.sh`): неожиданный код
|
||
утилиты (255) нормализуется в `4` («ошибка стенда»), нет прав — `3`, упавшие тесты — `1`.
|
||
|
||
### 6.2 Реальные прогоны
|
||
|
||
| прогон | команда | результат |
|
||
|---|---|---|
|
||
| стенд на хосте | `make test-netns` | exit 0, 8 ok / 0 fail, 5.46 s, `ip netns list` пуст, мок и sink не остались в процессах |
|
||
| стенд в контейнере (путь CI) | `make test-netns-ci` | exit 0, `[netns] вход в netns: nsenter`, backend `iptables`, 8 ok / 0 fail, 5.50 s, `[ ok ] топология снесена`, отчёт `/artifacts/netns-report.json` |
|
||
| нет прав | `docker run` без `--privileged` | exit **3**, `[blocked] не удалось создать netns — нет CAP_NET_ADMIN/SYS_ADMIN` |
|
||
| форс нерабочего режима | `AWG_NS_EXEC_MODE=ip` в контейнере | exit **4**, `mount of /sys failed` → `[warn] run.sh: непредвиденный код 255 — трактую как ошибку стенда (4)` |
|
||
| упавшие тесты | синтетический проваленный тест в контейнере | exit **1**, `[warn] netns-стенд: тесты/очистка упали (код 1)` |
|
||
| гейт покрытия | `make test-deploy` | exit 0, 51 passed, coverage **98.86 %** (`deploy/api_stub.py` 98 %, `tests/deploy/validate_compose.py` 99 %) |
|
||
| манифесты / shellcheck | `make test-manifests`, `make lint-shell` | exit 0 / exit 0 |
|
||
| стабильность в контейнере | 5 прогонов `make test-netns-ci` подряд | 5/5 exit 0, 8 ok каждый, ни одного «остался netns» |
|
||
| AC-1 после правок | `curl :8180/api/v1/health` | HTTP 200, `"stub": true`, контейнер `healthy` |
|
||
| AC-2 после правок | `make dev-verify VERIFY_ARGS="--no-up --skip-build"` | exit 0: маркеры `/config` и `/data` пережили restart и пересоздание, видны на хосте (bind mount) |
|
||
|
||
### 6.3 Что изменено для гейта покрытия
|
||
|
||
* `tests/deploy/pytest.ini` — отдельный профиль: `--cov=deploy --cov=tests/deploy
|
||
--cov-config=tests/deploy/.coveragerc --cov-fail-under=70`. Раньше `pytest tests/deploy`
|
||
наследовал addopts корневого `pyproject.toml` (`--cov=awg_agent --cov=awg_api`), поэтому
|
||
покрытие всегда было 0.00 % и RC=1 — гейт гоняли с `--no-cov` в обход порога.
|
||
* `tests/deploy/.coveragerc` — из покрытия исключены сами тестовые модули
|
||
(`tests/deploy/test_*.py`), иначе «покрытие» измеряет тестовый код.
|
||
* `tests/deploy/test_api_stub.py` — 9 тестов на живой HTTP-сервер заглушки: 200 и
|
||
`"stub": true` на `/api/v1/health`, алиас `/healthz`, игнор query-string, 404
|
||
`not_implemented` на прочие пути, HEAD, отказ на POST, разбор аргументов `main()`.
|
||
* `tests/deploy/test_validate_compose.py` — 39 негативных/граничных тестов валидатора:
|
||
каждая инварианта AC #21 ломается по отдельности (порт, тома, healthcheck, NET_ADMIN,
|
||
`/dev/net/tun`, `ip_forward`, privileged netns-сервис, DEV-ONLY-метка, `--require-dockerfile`).
|
||
* `deploy/api_stub.py` — `make_server()` выделен из `main()` (тесты поднимают сервер на порту 0).
|
||
* `make test-deploy` — гейт покрытия; `make test-netns-ci` прокидывает `AWG_NS_EXEC_MODE`.
|
||
* `topology.sh`: снос топологии удаляет namespace'ы безусловно и до 5 раз проверяет
|
||
результат — в контейнере `ip netns delete` иногда «терялся» и `run.sh` отдавал код 4
|
||
на зелёных тестах (флак 1 из 5 прогонов, после правки — 5/5 зелёных).
|
||
|
||
### 6.4 Что закрыто и что осталось
|
||
|
||
Закрыты оба блокера раунда 1 (`make test-netns-ci` работает в контейнере, покрытие
|
||
измеряется и ≥70 %). `CI green` по-прежнему недостижим: Gitea Actions runner не
|
||
зарегистрирован (#17), все прогоны в таблице 6.2 — ручные на `192.168.1.147`; команды
|
||
`make test-netns-ci` и `make test-deploy` — ровно те, что должны попасть в workflow #17.
|
||
|
||
Порт и stub-оверрайд (см. §4) без изменений: `privileged: true` и `cap_add SYS_MODULE`
|
||
из `deploy/docker-compose.test.yml` — только DEV/TEST, для staging/prod непригодны.
|