Merge branch 'main' into feat/18-docker-runtime
ci / lint (push) Successful in 51s
ci / lint (pull_request) Successful in 49s
contract-openapi / openapi-contract (push) Successful in 59s
contract-openapi / ts-client-is-current (push) Successful in 48s
contract-openapi / openapi-contract (pull_request) Successful in 59s
contract-openapi / ts-client-is-current (pull_request) Successful in 43s
ci / unit (push) Successful in 52s
ci / security (push) Successful in 1m21s
ci / unit (pull_request) Successful in 55s
ci / security (pull_request) Successful in 1m20s
ci / build (push) Successful in 36s
ci / build (pull_request) Successful in 36s
ci / image-scan (push) Failing after 3s
ci / image-scan (pull_request) Failing after 3s
ci / push (push) Has been skipped
ci / push (pull_request) Has been skipped
ci / lint (push) Successful in 51s
ci / lint (pull_request) Successful in 49s
contract-openapi / openapi-contract (push) Successful in 59s
contract-openapi / ts-client-is-current (push) Successful in 48s
contract-openapi / openapi-contract (pull_request) Successful in 59s
contract-openapi / ts-client-is-current (pull_request) Successful in 43s
ci / unit (push) Successful in 52s
ci / security (push) Successful in 1m21s
ci / unit (pull_request) Successful in 55s
ci / security (pull_request) Successful in 1m20s
ci / build (push) Successful in 36s
ci / build (pull_request) Successful in 36s
ci / image-scan (push) Failing after 3s
ci / image-scan (pull_request) Failing after 3s
ci / push (push) Has been skipped
ci / push (pull_request) Has been skipped
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# deploy — сборка и развёртывание
|
||||
|
||||
| Артефакт | Назначение | Задача |
|
||||
|---|---|---|
|
||||
| `Dockerfile` | multi-stage: amneziawg-go + awg-tools → runtime alpine | #18 |
|
||||
| `entrypoint.sh` | супервизор: `awg-agent` (root) + `awg-api` | #18 |
|
||||
| `docker-compose.dev.yml` | dev-стенд: порт 8080, тома `/config` `/data`, healthcheck | #21 |
|
||||
| `docker-compose.test.yml` | стенд для тестов | #21 |
|
||||
| `docker-compose.stub.yml` | DEV-ONLY оверрайд: health-заглушка до мержа #31 | #21 |
|
||||
| `api_stub.py` | минимальный `/api/v1/health` для проверки плумбинга стенда | #21 |
|
||||
| `verify-dev-stand.sh` | проверка AC #21 на Docker-хосте (`make dev-verify`) | #21 |
|
||||
| `dev-stand.md` | runbook dev-стенда: запуск, AC, откат, pitfalls | #21 |
|
||||
| `grafana/` | дашборд JSON (опциональный профиль monitoring) | #46 |
|
||||
| `awg0.conf.example` | пример клиентского конфига | #22 |
|
||||
|
||||
Запуск по ТЗ (после релиза): `--network=host`, `--device=/dev/net/tun`, `--cap-add=NET_ADMIN --cap-add=SYS_MODULE`, `--restart always`.
|
||||
|
||||
## Dev-стенд (задача #21)
|
||||
|
||||
```bash
|
||||
make dev-up # поднять стенд (compose project awg-dash-dev)
|
||||
make dev-verify # AC: health 200 + /config /data переживают restart
|
||||
make dev-up-stub # + health-заглушка, пока нет API (#31)
|
||||
make dev-logs
|
||||
make dev-down
|
||||
```
|
||||
|
||||
* Состояние: `/opt/awg-dash-dev/{config,data}` на Docker-хосте (`AWG_DASH_HOST_DIR`).
|
||||
* Хост-порт: `AWG_DASH_PORT` (по умолчанию 8180 — 8080 занят на dev-хосте; в контейнере всегда 8080).
|
||||
* Подробности, таблица AC, откат и pitfalls — [dev-stand.md](dev-stand.md).
|
||||
|
||||
### Тестовый стенд без реального провайдера
|
||||
|
||||
```bash
|
||||
make test-netns # netns-топология client/gw/uplink + мок WireGuard-endpoint
|
||||
make test-netns-ci # то же в привилегированном контейнере (путь для CI, #17)
|
||||
make test-manifests # статическая проверка compose-файлов (без Docker)
|
||||
make lint-shell # shellcheck для скриптов стенда
|
||||
```
|
||||
|
||||
Описание топологии, тестов и pitfalls — [../tests/netns/README.md](../tests/netns/README.md).
|
||||
@@ -0,0 +1,283 @@
|
||||
# Проверка 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. Стенд запускает именно то, что в ветке
|
||||
|
||||
Актуальные блобы для раунда 3 — в §7.3 (эта таблица фиксирует состояние раунда 2).
|
||||
|
||||
Каталог стенда `(/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` | `6c13a7cfd4d9368ee7d6cb2efca85d890b803e59` |
|
||||
| `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`.
|
||||
|
||||
**Контракт действует на уровне контейнера, а не через make.** GNU make возвращает `2`
|
||||
на любой упавший рецепт, поэтому `make test-netns-ci` при коде контейнера `3` или `4`
|
||||
сам отдаёт `2` (в выводе остаётся только `make: *** [Makefile:…] Error 4`). Для CI #17
|
||||
добавлена точка входа без make — `bash tests/netns/ci.sh`: она запускает тот же контейнер
|
||||
(`docker compose … run --rm -T netns`) и возвращает его код как есть. Проверено в §7.2.
|
||||
|
||||
### 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` — гейт покрытия. Утверждение раунда 2 «`make test-netns-ci` прокидывает
|
||||
`AWG_NS_EXEC_MODE`» было **неверным**: make-алиас `NETNS_EXEC_MODE ?= auto` перекрывал
|
||||
переменную. Исправлено в §7.1 (проверено прогоном в §7.2).
|
||||
* `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 непригодны.
|
||||
|
||||
## 7. Раунд 3 (ревью PR #53, раунд 2): контракт кодов, env-алиас, сборка без сети
|
||||
|
||||
Дата: 2026-10-28 · Хост: `192.168.1.147` (Debian 13, Docker 29.4.1, compose v5.1.3, kernel 6.17)
|
||||
Ветка: `feat/21-dev-stand` · Ref: #21, #15, #17, #56
|
||||
|
||||
Ревью qa-engineer (раунд 2, PR #53 `issuecomment-664`) дало три воспроизводимых замечания:
|
||||
контракт `0/1/3/4` теряется на уровне make, документированный `AWG_NS_EXEC_MODE` перекрыт
|
||||
make-алиасом, `make dev-up` падает при сборке без DNS. Все три закрыты правками ниже.
|
||||
|
||||
### 7.1 Что изменено
|
||||
|
||||
| замечание | правка |
|
||||
|---|---|
|
||||
| `make test-netns-ci` отдаёт `2` на любом провале → CI #17 не отличит 3/4/1 | новая точка входа `tests/netns/ci.sh`: `docker compose -f deploy/docker-compose.test.yml run --rm -T netns`, код контейнера возвращается как есть (`2` у неё = сломан вызов/нет docker/образа). `make test-netns-ci` делегирует в неё, в комментарии таргета и в §6.1 зафиксировано «make → 2» |
|
||||
| `AWG_NS_EXEC_MODE=ip make test-netns-ci` игнорировался (`Makefile:19` `NETNS_EXEC_MODE ?= auto` + `:117`) | `NETNS_EXEC_MODE ?= $(if $(AWG_NS_EXEC_MODE),$(AWG_NS_EXEC_MODE),auto)` — документированное имя имеет приоритет, make-алиас остался для совместимости; README и `dev-stand.md` приведены к факту |
|
||||
| `make dev-up` форсит `--build` и падает без DNS | `DEV_SKIP_BUILD=1` → `up -d --no-build`; `DEV_UP_BUILD` переопределяем (`?=`) и по-прежнему принимается из командной строки (`DEV_UP_BUILD=`); строка в `dev-stand.md` (быстрый старт + pitfalls + таблица переменных) |
|
||||
|
||||
### 7.2 Реальные прогоны (дерево сверено по git-blob-SHA, см. §7.3)
|
||||
|
||||
| прогон | команда | код | что видно в выводе |
|
||||
|---|---|---|---|
|
||||
| CI-точка входа, зелёный | `bash tests/netns/ci.sh` | **0** | `[ci] образ=awg-dash:dev режим netns=auto`, `вход в netns: nsenter`, `Ran 8 tests`, `8 ok`, `[ ok ] топология снесена`, `[ci][ ok ]` |
|
||||
| make-путь, зелёный | `make test-netns-ci` | **0** | те же 8 ok через `ci.sh` |
|
||||
| код 4 (форс нерабочего режима) | `AWG_NS_EXEC_MODE=ip bash tests/netns/ci.sh` | **4** | `[ci] … режим netns=ip`, `mount of /sys failed` → `[warn] код 255 → 4` |
|
||||
| код 3 (нет прав на netns) | `AWG_NS_PRIVILEGED=0 bash tests/netns/ci.sh` | **3** | `[blocked] не удалось создать netns — нет CAP_NET_ADMIN/SYS_ADMIN`, `[ci][blocked]` |
|
||||
| код 1 (упавшие тесты) | синтетический падающий тест в `tests/netns/` → `bash tests/netns/ci.sh` | **1** | `Ran 9 tests`, `FAILED (failures=1)`, `8 ok, 1 fail` (тест удалён после прогона) |
|
||||
| контракт make при коде 4 | `AWG_NS_EXEC_MODE=ip make test-netns-ci` | **2** (контейнер 4) | `[ci] … режим netns=ip` — **env больше не перекрывается**; `make: *** [Makefile:133: test-netns-ci] Error 4` |
|
||||
| контракт make при коде 1 | синтетический падающий тест → `make test-netns-ci` | **2** (контейнер 1) | `make: *** [Makefile:133: test-netns-ci] Error 1` |
|
||||
| AC-1 | `make dev-up DEV_SKIP_BUILD=1` + `curl :8180/api/v1/health` | **0**, HTTP **200** | `up -d --no-build`, контейнер `Up (healthy)`, ответ `"stub": true` |
|
||||
| AC-1 со сборкой (кеш тёплый) | `make dev-up` (без `DEV_SKIP_BUILD`) | **0** | `222` шагов `CACHED`, `Image awg-dash:dev Built`, контейнер пересоздан и `healthy` — на холодном кеше этот путь требует DNS, поэтому кноб нужен |
|
||||
| AC-2 | `make dev-verify VERIFY_ARGS="--no-up --skip-build"` | **0** | маркеры `/config` и `/data` пережили `restart` и `--force-recreate`, видны на хосте (bind mount) |
|
||||
| гейт покрытия | `make test-deploy` | **0** | `51 passed`, `Required test coverage of 70% reached. Total coverage: 98.86%` (`TOTAL 176 строк, 2 miss`) |
|
||||
| манифесты | `make test-manifests` | **0** | `validate-compose: ok (1 предупреждений)` — предупреждение про `deploy/Dockerfile` (до мержа #18) |
|
||||
| shellcheck | `make lint-shell` | **0** | проверен и новый `tests/netns/ci.sh` |
|
||||
| уборка | `ip netns list`; `docker ps -a --filter name=awg-dash-test` | — | пусто, контейнеров после прогонов не осталось |
|
||||
|
||||
Логи прогонов на стенд-хосте: `/tmp/r3_a.log` … `/tmp/r3_m.log`
|
||||
(`a` — зелёный `ci.sh`, `b` — код 4, `c` — код 3, `d` — make при коде 4, `e`/`f` — код 1 через `ci.sh`/make,
|
||||
`g` — `make test-netns-ci`, `h` — `dev-up DEV_SKIP_BUILD=1` + health, `i` — AC-2, `j` — `test-deploy`,
|
||||
`k` — `test-manifests`, `l` — `lint-shell`, `m` — `dev-up` со сборкой).
|
||||
|
||||
### 7.3 Блобы: файлы на стенд-хосте == дерево ветки
|
||||
|
||||
| файл | git blob |
|
||||
|---|---|
|
||||
| `Makefile` | `55c3ea5c98c7980c9bc3fcec8483d1140e670b2e` |
|
||||
| `tests/netns/ci.sh` | `561ca3976ad3749fbde79b8d163e007bace145de` |
|
||||
| `tests/netns/README.md` | `a4642f845f38fca1da0019af7e91d95be9e4d97c` |
|
||||
| `deploy/dev-stand.md` | `60495f7cd8eba10b40dc9aef469bea210b50e8e1` |
|
||||
| `deploy/VERIFICATION-21.md` | этот файл (блоб меняется этим же коммитом) |
|
||||
| остальные файлы стенда | без изменений с раунда 2 (§0) |
|
||||
|
||||
### 7.4 Что осталось
|
||||
|
||||
* **CI green (DoD)** — за задачей #17: у репозитория появился runner (контейнер
|
||||
`gitea-runner-awg-repo`, запуски runs 22–31), но workflow, покрывающего ветку #21, в `main`
|
||||
ещё нет, поэтому все гейты выше прогнаны вручную. Команды для workflow:
|
||||
`bash tests/netns/ci.sh`, `make test-deploy`, `make test-manifests`, `make lint-shell` —
|
||||
они уже отдают нужные коды без make-обёртки.
|
||||
* Merge в `main` и любой prod/staging-деплой — за TechLead (ADVISORY): автор ревью не мержит.
|
||||
* `verify-dev-stand.sh` после `--force-recreate` печатает статус до перехода в `healthy`,
|
||||
но AC-проверки при этом ждут health (`wait_health 20`) и прогон возвращает `0` — на вердикт
|
||||
не влияет.
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""api_stub.py — DEV-ONLY заглушка health-эндпоинта (задача #21).
|
||||
|
||||
Существует ровно для одной цели: до реализации FastAPI-каркаса (#31) проверить
|
||||
плумбинг dev-стенда — маппинг порта, healthcheck контейнера, тома /config и
|
||||
/data, переживание `docker compose restart`.
|
||||
|
||||
Отдаёт:
|
||||
GET /api/v1/health -> 200 {"status":"ok","stub":true,...}
|
||||
GET /healthz -> 200 (алиас для отладки)
|
||||
всё остальное -> 404 {"error":"not_implemented","issue":"#31"}
|
||||
|
||||
Чего НЕ делает: никакой логики шлюза, никакого доступа к awg/iptables.
|
||||
В staging/prod запускать запрещено (см. deploy/dev-stand.md).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
STARTED_AT = time.time()
|
||||
STUB_ENV = os.environ.get("AWG_API_STUB", "0")
|
||||
|
||||
HEALTH_PATHS = ("/api/v1/health", "/healthz")
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "awg-dash-stub/0.1"
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def _json(self, code: int, payload: dict) -> None:
|
||||
body = (json.dumps(payload, sort_keys=True) + "\n").encode()
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802 (HTTP-метод)
|
||||
path = self.path.split("?", 1)[0]
|
||||
if path in HEALTH_PATHS:
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"status": "ok",
|
||||
"stub": True,
|
||||
"issue": "#31",
|
||||
"note": "заглушка dev-стенда: реальный API появится в #31",
|
||||
"uptime_s": round(time.time() - STARTED_AT, 3),
|
||||
"stub_env": STUB_ENV,
|
||||
},
|
||||
)
|
||||
return
|
||||
self._json(404, {"error": "not_implemented", "issue": "#31", "path": path})
|
||||
|
||||
def do_HEAD(self) -> None: # noqa: N802
|
||||
self.send_response(200 if self.path.split("?", 1)[0] in HEALTH_PATHS else 404)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None: # лог в stdout контейнера
|
||||
print(f"[api-stub] {self.address_string()} {fmt % args}", flush=True)
|
||||
|
||||
|
||||
def make_server(host: str, port: int) -> ThreadingHTTPServer:
|
||||
"""HTTP-сервер заглушки.
|
||||
|
||||
Вынесен из main(), чтобы тесты (tests/deploy/test_api_stub.py) поднимали
|
||||
настоящий HTTP-сервер на свободном порту (port=0) и проверяли коды ответов.
|
||||
"""
|
||||
return ThreadingHTTPServer((host, port), Handler)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="DEV-ONLY заглушка health для dev-стенда (#21)")
|
||||
ap.add_argument("--host", default="0.0.0.0")
|
||||
ap.add_argument("--port", type=int, default=8080)
|
||||
ap.add_argument("--domain", default="/config", help="каталог, доступность которого проверяем (для лога)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
print(
|
||||
f"[api-stub] DEV-ONLY заглушка на {args.host}:{args.port}; каталог {args.domain} "
|
||||
f"{'доступен' if os.path.isdir(args.domain) else 'НЕ доступен'}",
|
||||
flush=True,
|
||||
)
|
||||
make_server(args.host, args.port).serve_forever()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,127 @@
|
||||
# dev/staging стенд awg-dash — runbook
|
||||
|
||||
Задача **#21** (эпик **#15**). Стенд для локальной разработки и проверки
|
||||
acceptance criteria до появления staging-окружения.
|
||||
|
||||
* Docker-хост: **192.168.1.147** (`ssh root@192.168.1.147`), Docker 29.4.x + compose v5.
|
||||
* Каталог состояния: **`/opt/awg-dash-dev/{config,data}`** (bind mount → `/config`, `/data`).
|
||||
* Compose project: **`awg-dash-dev`** (изолирован от PoC-контейнеров `amneziawg-client`).
|
||||
* Хост-порт приложения: **8180** (`AWG_DASH_PORT`), внутри контейнера — **8080**.
|
||||
Порт 8080 на dev-хосте занят другим сервисом (drawio), поэтому проброс нестандартный.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
# на Docker-хосте, из корня репозитория
|
||||
export AWG_DASH_HOST_DIR=/opt/awg-dash-dev
|
||||
make dev-up # docker compose up -d --build (проект awg-dash-dev)
|
||||
make dev-verify # проверка AC: health 200 + состояние переживает restart
|
||||
curl -fsS http://127.0.0.1:8180/api/v1/health
|
||||
make dev-logs # логи контейнера
|
||||
make dev-down # остановить (каталоги состояния сохраняются)
|
||||
```
|
||||
|
||||
`dev-up` добавляет `--build`, когда в дереве есть `deploy/Dockerfile` (после мержа #18).
|
||||
Если у хоста нет сети/DNS для `docker build` (офлайн-стенд, CI без прокси), сборку
|
||||
нужно пропустить — образ `awg-dash:dev` уже должен существовать:
|
||||
|
||||
```bash
|
||||
make dev-up DEV_SKIP_BUILD=1 # up -d --no-build — использовать готовый образ
|
||||
make dev-up DEV_UP_BUILD= # вообще не передавать флаги сборки
|
||||
```
|
||||
|
||||
До реализации FastAPI-каркаса (**#31**) `/api/v1/health` не отвечает — контейнер
|
||||
работает как агент и помечается `unhealthy`. Чтобы проверить плумбинг стенда
|
||||
(порт, healthcheck, тома, restart) **до** #31, используйте health-заглушку:
|
||||
|
||||
```bash
|
||||
make dev-up-stub # + deploy/docker-compose.stub.yml (DEV-ONLY, api_stub.py)
|
||||
make dev-verify-stub # AC-1 проходит на заглушке, в отчёте api=stub
|
||||
```
|
||||
|
||||
Заглушка отвечает `{"status":"ok","stub":true,...}` только на `/api/v1/health` и
|
||||
никогда не должна использоваться в staging/prod.
|
||||
|
||||
## Проверка AC (#21)
|
||||
|
||||
| AC | Команда | Ожидание |
|
||||
|---|---|---|
|
||||
| AC-1: health 200 | `curl -fsS -o /dev/null -w '%{http_code}' http://127.0.0.1:8180/api/v1/health` | `200` (после мержа #31; до него — заглушка через `dev-verify-stub`) |
|
||||
| AC-2: состояние переживает restart | `make dev-verify` | маркеры в `/config` и `/data` совпадают до и после `docker compose restart` и `--force-recreate` |
|
||||
| AC-3: netns + мок-endpoint | `make test-netns` | 8/8 тестов, netns `awgt21-{client,gw,uplink}` создаются и сносятся |
|
||||
|
||||
`deploy/verify-dev-stand.sh` (за `make dev-verify`) печатает отчёт и возвращает:
|
||||
|
||||
| Код | Значение |
|
||||
|---|---|
|
||||
| 0 | AC-1 и AC-2 выполнены |
|
||||
| 1 | регрессия стенда (health не 200 с заглушкой, состояние не сохранилось) |
|
||||
| 2 | AC-1 заблокирован: API (#31) ещё не реализован, стенд при этом поднят |
|
||||
| 3 | заблокировано окружением: нет docker/compose или compose-файлов |
|
||||
|
||||
## Состав стенда
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `docker-compose.dev.yml` | приложение: порт 8080 (наружу `${AWG_DASH_PORT:-8180}`), bind mount `/config` `/data`, healthcheck, NET_ADMIN/NET_RAW/SYS_MODULE, `/dev/net/tun` |
|
||||
| `docker-compose.stub.yml` | DEV-ONLY оверрайд: health-заглушка до #31 (`api_stub.py`) |
|
||||
| `docker-compose.test.yml` | тестовый стенд: привилегированный `netns`-раннер + `app` для API-тестов |
|
||||
| `api_stub.py` | минимальный HTTP-сервер `/api/v1/health` (только dev) |
|
||||
| `verify-dev-stand.sh` | проверка AC на Docker-хосте + JSON-отчёт |
|
||||
| `../tests/netns/` | netns-харнесс: топология client/gw/uplink + мок WireGuard-endpoint |
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
| Переменная | По умолчанию | Назначение |
|
||||
|---|---|---|
|
||||
| `AWG_DASH_PORT` | `8180` | хост-порт приложения |
|
||||
| `AWG_DASH_HOST_DIR` | `/opt/awg-dash-dev` | каталог состояния на хосте |
|
||||
| `AWG_DASH_IMAGE` | `awg-dash:dev` | имя образа (build или pull) |
|
||||
| `AWG_DASH_DOCKERFILE` | `deploy/Dockerfile` | путь к Dockerfile для build |
|
||||
| `COMPOSE_PROJECT_NAME` | `awg-dash-dev` | изоляция от других стендов |
|
||||
| `AWG_LOG_LEVEL` | `info` | уровень логирования приложения |
|
||||
| `DEV_SKIP_BUILD` | — | `1` → `make dev-up` выполняет `up -d --no-build` (образ уже собран) |
|
||||
| `DEV_UP_BUILD` | авто | переопределение флагов сборки (`--build`, пусто, `--no-build`) |
|
||||
| `AWG_NS_EXEC_MODE` | `auto` | режим входа в netns: `auto|ip|nsenter` (issue #56) |
|
||||
| `NETNS_EXEC_MODE` | `auto` | make-алиас `AWG_NS_EXEC_MODE` (приоритет у `AWG_NS_EXEC_MODE`) |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Зависимость | Состояние | Влияние на стенд |
|
||||
|---|---|---|
|
||||
| #16 (каркас репо, Makefile) | PR #47 | нужен `Makefile`, `requirements.txt`, `deploy/` |
|
||||
| #18 (образ, entrypoint) | PR #49 | нужен `deploy/Dockerfile` для `--build` |
|
||||
| #31 (каркас FastAPI, `/api/v1/health`) | Sprint 2 | без него AC-1 выполняется только с заглушкой |
|
||||
| #17 (CI-пайплайн) | Sprint 0 | гейты для пайплайна: `bash tests/netns/ci.sh` (коды 0/1/3/4), `make test-manifests`, `make test-deploy`, `make lint-shell` |
|
||||
|
||||
## Откат и уборка
|
||||
|
||||
```bash
|
||||
make dev-down # остановить контейнеры проекта awg-dash-dev
|
||||
make dev-reset # down --remove-orphans (состояние сохраняется)
|
||||
docker image rm awg-dash:dev # откатить образ (пересобрать из предыдущего коммита)
|
||||
du -sh /opt/awg-dash-dev # состояние стенда
|
||||
```
|
||||
|
||||
Перед откатом образа проверьте проект: `docker compose -p awg-dash-dev ls`.
|
||||
Состояние (`/opt/awg-dash-dev`) не удаляется автоматически — это осознанно: там
|
||||
конфиги туннеля (`/config/awg0.conf`). Удаляйте вручную и только после бэкапа.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
* **Порт 8080 занят** на dev-хосте — наружу публикуется 8180. Меняйте `AWG_DASH_PORT`.
|
||||
* **`/dev/net/tun` обязателен**: без него entrypoint падает (`fail '/dev/net/tun не проброшен'`).
|
||||
* **Права на каталоги состояния**: entrypoint делает `chown awg:awg /config /data`;
|
||||
каталоги создаются на хосте от root — не меняйте владельца на хосте вручную.
|
||||
* **`docker compose restart` сохраняет тома**; `down -v` — нет. Никогда не используйте
|
||||
`down -v` на этом стенде: это удалит `/config` и `/data`.
|
||||
* **Заглушка `api_stub.py`** — только dev: она отвечает 200 на `/api/v1/health` и
|
||||
маскирует отсутствие API. Не включайте `docker-compose.stub.yml` в staging.
|
||||
* **netns-стенд вне Docker** требует root: `make test-netns` вернёт код 3, если нет
|
||||
CAP_NET_ADMIN/CAP_SYS_ADMIN (например, в непривилегированном CI-раннере).
|
||||
* **netns в контейнере (`make test-netns-ci`, `bash tests/netns/ci.sh`)**: `ip netns exec`
|
||||
падает (`mount of /sys failed`), харнесс сам переходит на `nsenter`; контракт кодов
|
||||
`0/1/3/4` действует на уровне контейнера. **make возвращает 2 на любом провале** —
|
||||
в CI вызывайте `bash tests/netns/ci.sh`, иначе ветки 3 и 4 неразличимы (#56).
|
||||
* **`--build` без сети**: после мержа #18 `dev-up` собирает образ; если на хосте нет
|
||||
DNS/доступа к registry, используйте `DEV_SKIP_BUILD=1` (готовый `awg-dash:dev`).
|
||||
@@ -0,0 +1,69 @@
|
||||
# dev/staging стенд awg-dash (задача #21, эпик #15).
|
||||
#
|
||||
# Отдельный compose-project `awg-dash-dev` — не пересекается с контейнерами PoC
|
||||
# (amneziawg-client) и другими сервисами dev-хоста.
|
||||
#
|
||||
# Запуск: make dev-up (поднимает стек с образом awg-dash:dev)
|
||||
# Проверка AC: make dev-verify (health 200 + переживание restart для /config /data)
|
||||
# Логи: make dev-logs
|
||||
# Остановка: make dev-down (тома/каталоги состояния сохраняются)
|
||||
#
|
||||
# AC-1 (health 200) до реализации FastAPI-каркаса (#31): сам образ API не поднимает,
|
||||
# поэтому `make dev-up` автоматически добавляет оверрайд с health-заглушкой
|
||||
# (deploy/docker-compose.stub.yml, ответ помечен "stub": true) и стенд проходит
|
||||
# healthcheck. После появления backend/awg_api/app.py оверрайд не добавляется.
|
||||
# Явные режимы: `make dev-up-app` (только реальный образ), `make dev-up-stub`.
|
||||
|
||||
name: ${COMPOSE_PROJECT_NAME:-awg-dash-dev}
|
||||
|
||||
services:
|
||||
awg-dash:
|
||||
image: ${AWG_DASH_IMAGE:-awg-dash:dev}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ${AWG_DASH_DOCKERFILE:-deploy/Dockerfile}
|
||||
args:
|
||||
AMNEZIAWG_GO_REF: ${AMNEZIAWG_GO_REF:-master}
|
||||
AMNEZIAWG_TOOLS_REF: ${AMNEZIAWG_TOOLS_REF:-master}
|
||||
container_name: awg-dash-dev
|
||||
restart: unless-stopped
|
||||
# Порт приложения внутри образа всегда 8080 (EXPOSE/AWG_API_PORT).
|
||||
# На dev-хосте 192.168.1.147 порт 8080 занят (drawio) — по умолчанию 8180.
|
||||
ports:
|
||||
- "${AWG_DASH_PORT:-8180}:8080"
|
||||
volumes:
|
||||
# Состояние вынесено на хост, чтобы правки конфигурации переживали
|
||||
# `docker compose restart` (AC задачи #21) и пересоздание контейнера.
|
||||
- ${AWG_DASH_HOST_DIR:-/opt/awg-dash-dev}/config:/config
|
||||
- ${AWG_DASH_HOST_DIR:-/opt/awg-dash-dev}/data:/data
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
cap_add:
|
||||
# NET_ADMIN — iptables/ipset/ip rule; NET_RAW — ping/socket-проверки;
|
||||
# SYS_MODULE — sysctl net.ipv4.ip_forward и модули ядра (как в ТЗ).
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
- SYS_MODULE
|
||||
sysctls:
|
||||
net.ipv4.ip_forward: "1"
|
||||
net.ipv4.conf.all.src_valid_mark: "1"
|
||||
environment:
|
||||
AWG_CONFIG_DIR: /config
|
||||
AWG_DATA_DIR: /data
|
||||
AWG_RUN_DIR: /run/awg
|
||||
AWG_API_HOST: 0.0.0.0
|
||||
AWG_API_PORT: "8080"
|
||||
AWG_LOG_LEVEL: ${AWG_LOG_LEVEL:-info}
|
||||
AWG_DEV_STAND: "1"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/api/v1/health >/dev/null || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 4
|
||||
stop_grace_period: 20s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,23 @@
|
||||
# Оверрайд для dev-стенда: health-заглушка вместо ещё не реализованного API.
|
||||
#
|
||||
# Зачем: AC-1 задачи #21 (`curl :8080/api/v1/health` -> 200) физически не может
|
||||
# выполниться до появления FastAPI-каркаса (#31, Sprint 2). Чтобы проверить
|
||||
# плумбинг стенда (порт, healthcheck, тома, restart) уже сейчас, этот оверрайд
|
||||
# подменяет entrypoint на минимальный HTTP-сервер из deploy/api_stub.py.
|
||||
#
|
||||
# Это НЕ приложение: заглушка отдаёт 200 только на /api/v1/health и явно
|
||||
# помечает ответ `"stub": true`. В prod/staging не использовать.
|
||||
#
|
||||
# Запуск: docker compose -f deploy/docker-compose.dev.yml -f deploy/docker-compose.stub.yml up -d
|
||||
# (или make dev-up-stub / make dev-verify-stub)
|
||||
#
|
||||
# После мержа #31 оверрайд не нужен: `make dev-up` поднимает реальный API.
|
||||
|
||||
services:
|
||||
awg-dash:
|
||||
entrypoint: ["python3", "/opt/awg-dash-stub/api_stub.py"]
|
||||
command: ["--host", "0.0.0.0", "--port", "8080", "--domain", "/config"]
|
||||
volumes:
|
||||
- ../deploy/api_stub.py:/opt/awg-dash-stub/api_stub.py:ro
|
||||
environment:
|
||||
AWG_API_STUB: "1"
|
||||
@@ -0,0 +1,73 @@
|
||||
# Тестовый стенд awg-dash (задача #21): netns-топология + мок WireGuard-endpoint.
|
||||
#
|
||||
# Использование:
|
||||
# docker compose -f deploy/docker-compose.test.yml run --rm netns # прогон стенда
|
||||
# docker compose -f deploy/docker-compose.test.yml run --rm netns --keep-topology
|
||||
# docker compose -f deploy/docker-compose.test.yml up -d app # приложение для API-тестов
|
||||
# (make test-netns-ci — обёртка над первым вызовом)
|
||||
#
|
||||
# Контейнеру `netns` нужен --privileged: внутри создаются сетевые namespace
|
||||
# (`ip netns add`) и veth-пары, для этого требуются CAP_SYS_ADMIN + CAP_NET_ADMIN.
|
||||
# Репозиторий монтируется только для чтения; артефакты пишутся в /artifacts и /tmp.
|
||||
|
||||
name: ${COMPOSE_PROJECT_NAME:-awg-dash-test}
|
||||
|
||||
services:
|
||||
netns:
|
||||
image: ${AWG_DASH_IMAGE:-awg-dash:dev}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ${AWG_DASH_DOCKERFILE:-deploy/Dockerfile}
|
||||
container_name: awg-dash-test-netns
|
||||
privileged: true
|
||||
working_dir: /repo
|
||||
entrypoint: ["/bin/bash", "/repo/tests/netns/run.sh"]
|
||||
command: ["--report", "/artifacts/netns-report.json"]
|
||||
volumes:
|
||||
- ..:/repo:ro
|
||||
- ${AWG_DASH_TEST_ARTIFACTS:-./artifacts}:/artifacts
|
||||
environment:
|
||||
AWG_NETNS_WORKDIR: /tmp/awg-netns
|
||||
AWG_NETNS_PREFIX: ${AWG_NETNS_PREFIX:-awgt21}
|
||||
# nft в образе приложения нет — там iptables (см. tests/netns/README.md)
|
||||
AWG_FW_BACKEND: ${AWG_FW_BACKEND:-auto}
|
||||
# В контейнере `ip netns exec` падает (mount of /sys, issue #56) — харнесс
|
||||
# сам переключается на nsenter --net; переменная позволяет форсировать режим.
|
||||
AWG_NS_EXEC_MODE: ${AWG_NS_EXEC_MODE:-auto}
|
||||
tmpfs:
|
||||
- /tmp:mode=1777
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "5m"
|
||||
max-file: "2"
|
||||
|
||||
app:
|
||||
image: ${AWG_DASH_IMAGE:-awg-dash:dev}
|
||||
container_name: awg-dash-test-app
|
||||
restart: "no"
|
||||
volumes:
|
||||
- awg-dash-test-config:/config
|
||||
- awg-dash-test-data:/data
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
- SYS_MODULE
|
||||
environment:
|
||||
AWG_CONFIG_DIR: /config
|
||||
AWG_DATA_DIR: /data
|
||||
AWG_API_HOST: 0.0.0.0
|
||||
AWG_API_PORT: "8080"
|
||||
AWG_TEST_STAND: "1"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/api/v1/health >/dev/null || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
awg-dash-test-config:
|
||||
awg-dash-test-data:
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-dev-stand.sh — проверка acceptance criteria задачи #21 на Docker-хосте.
|
||||
#
|
||||
# Запускать из корня репозитория на машине с Docker:
|
||||
# bash deploy/verify-dev-stand.sh # реальный API (после мержа #31)
|
||||
# bash deploy/verify-dev-stand.sh --stub # проверка плумбинга до #31 (health-заглушка)
|
||||
#
|
||||
# Что проверяет:
|
||||
# AC-1: `curl http://<host>:<port>/api/v1/health` -> 200;
|
||||
# AC-2: правки в /config и /data переживают `docker compose restart`
|
||||
# (и дополнительно пересоздание контейнера);
|
||||
# пласт: монтирования /config и /data указывают на ожидаемый каталог хоста,
|
||||
# контейнер healthy, образ и compose-файлы на месте.
|
||||
#
|
||||
# Коды возврата:
|
||||
# 0 — AC-1 и AC-2 выполнены;
|
||||
# 1 — AC нарушены (регрессия стенда);
|
||||
# 2 — AC-1 заблокирован: API ещё не реализован (#31), стенд при этом поднят;
|
||||
# 3 — заблокировано окружением (нет docker/compose/файлов).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_DEV="$ROOT/deploy/docker-compose.dev.yml"
|
||||
COMPOSE_STUB="$ROOT/deploy/docker-compose.stub.yml"
|
||||
|
||||
STUB=0
|
||||
DO_UP=1
|
||||
DO_BUILD=1
|
||||
KEEP_STAND=0
|
||||
HOST="127.0.0.1"
|
||||
PORT="${AWG_DASH_PORT:-8180}"
|
||||
HOST_DIR="${AWG_DASH_HOST_DIR:-/opt/awg-dash-dev}"
|
||||
JSON_OUT=""
|
||||
HEALTH_PATH="/api/v1/health"
|
||||
|
||||
usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--stub) STUB=1 ;;
|
||||
--no-up) DO_UP=0 ;;
|
||||
--skip-build) DO_BUILD=0 ;;
|
||||
--keep) KEEP_STAND=1 ;;
|
||||
--host) HOST="${2:?}"; shift ;;
|
||||
--port) PORT="${2:?}"; shift ;;
|
||||
--host-dir) HOST_DIR="${2:?}"; shift ;;
|
||||
--compose) COMPOSE_DEV="${2:?}"; shift ;;
|
||||
--json) JSON_OUT="${2:?}"; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "неизвестная опция: $1" >&2; exit 3 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
fatal_env() { printf '[verify][blocked] %s\n' "$*" >&2; exit 3; }
|
||||
say() { printf '[verify] %s\n' "$*"; }
|
||||
ok() { printf '[verify][ ok ] %s\n' "$*"; }
|
||||
bad() { printf '[verify][fail] %s\n' "$*" >&2; }
|
||||
|
||||
# --- предусловия -------------------------------------------------------------
|
||||
command -v docker >/dev/null 2>&1 || fatal_env "нет docker в PATH"
|
||||
docker compose version >/dev/null 2>&1 || fatal_env "нет плагина 'docker compose'"
|
||||
[ -f "$COMPOSE_DEV" ] || fatal_env "нет файла $COMPOSE_DEV"
|
||||
|
||||
COMPOSE_ARGS=(-f "$COMPOSE_DEV")
|
||||
if [ "$STUB" -eq 1 ]; then
|
||||
[ -f "$COMPOSE_STUB" ] || fatal_env "нет файла $COMPOSE_STUB"
|
||||
COMPOSE_ARGS+=(-f "$COMPOSE_STUB")
|
||||
fi
|
||||
compose() { docker compose "${COMPOSE_ARGS[@]}" "$@"; }
|
||||
|
||||
AC1="not_run"
|
||||
AC2="not_run"
|
||||
API_MODE="unknown"
|
||||
NOTES=()
|
||||
FAILURES=0
|
||||
|
||||
# --- поднятие стенда ---------------------------------------------------------
|
||||
if [ "$DO_UP" -eq 1 ]; then
|
||||
UP_ARGS=(up -d)
|
||||
[ "$DO_BUILD" -eq 1 ] && UP_ARGS+=(--build)
|
||||
say "docker compose ${COMPOSE_ARGS[*]} ${UP_ARGS[*]}"
|
||||
compose "${UP_ARGS[@]}" || fatal_env "не удалось поднять dev-стенд (см. вывод docker compose)"
|
||||
fi
|
||||
|
||||
compose ps >/dev/null 2>&1 || fatal_env "compose project не поднят"
|
||||
cid() { compose ps -q awg-dash | head -n 1; }
|
||||
CID="$(cid)"
|
||||
[ -n "$CID" ] || fatal_env "контейнер awg-dash не найден в compose project"
|
||||
|
||||
wait_health() {
|
||||
local tries="${1:-30}" code i
|
||||
for ((i = 1; i <= tries; i++)); do
|
||||
code="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "http://${HOST}:${PORT}${HEALTH_PATH}" || true)"
|
||||
[ "$code" = "200" ] && return 0
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- AC-1: health 200 --------------------------------------------------------
|
||||
BODY="$(curl -s -m 3 "http://${HOST}:${PORT}${HEALTH_PATH}" || true)"
|
||||
if wait_health 20; then
|
||||
BODY="$(curl -s -m 3 "http://${HOST}:${PORT}${HEALTH_PATH}" || true)"
|
||||
if printf '%s' "$BODY" | grep -q '"stub": *true'; then
|
||||
API_MODE="stub"
|
||||
NOTES+=("health отвечает заглушка (#31): реальный API ещё не смёржен")
|
||||
ok "AC-1: http://${HOST}:${PORT}${HEALTH_PATH} -> 200 (api=stub)"
|
||||
else
|
||||
API_MODE="real"
|
||||
ok "AC-1: http://${HOST}:${PORT}${HEALTH_PATH} -> 200 (api=real)"
|
||||
fi
|
||||
AC1="pass"
|
||||
else
|
||||
CODE="$(curl -s -o /dev/null -m 3 -w '%{http_code}' "http://${HOST}:${PORT}${HEALTH_PATH}" || echo "000")"
|
||||
say "health-check вернул ${CODE}; последние логи контейнера:"
|
||||
compose logs --tail=15 awg-dash || true
|
||||
if [ "$STUB" -eq 0 ]; then
|
||||
AC1="blocked"
|
||||
NOTES+=("AC-1 заблокирован: FastAPI-каркас (#31) ещё не реализован — /api/v1/health не отвечает")
|
||||
printf '[verify][blocked] AC-1 требует #31 (см. deploy/dev-stand.md)\n' >&2
|
||||
else
|
||||
AC1="fail"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
bad "AC-1: даже с заглушкой health не 200 (код ${CODE})"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- AC-2: состояние переживает restart --------------------------------------
|
||||
if [ "$AC1" = "blocked" ]; then
|
||||
NOTES+=("AC-2 проверяем без API: маркерные файлы в /config и /data")
|
||||
fi
|
||||
|
||||
STAMP="$(date +%s)"
|
||||
MARKER_TEXT="awg-dash-ac2-${STAMP}"
|
||||
say "AC-2: пишу маркеры в /config и /data и перезапускаю контейнер"
|
||||
if compose exec -T awg-dash sh -c "printf '%s\n' '$MARKER_TEXT' > /config/.ac2-marker && printf '%s\n' '$MARKER_TEXT' > /data/.ac2-marker"; then
|
||||
# 1) restart (как в AC)
|
||||
compose restart awg-dash >/dev/null
|
||||
if [ "$AC1" = "pass" ]; then wait_health 20 || NOTES+=("после restart health не поднялся за 40с"); fi
|
||||
CFG_AFTER="$(compose exec -T awg-dash cat /config/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
|
||||
DATA_AFTER="$(compose exec -T awg-dash cat /data/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
|
||||
|
||||
# 2) пересоздание контейнера (сильнее, чем restart)
|
||||
compose up -d --force-recreate >/dev/null
|
||||
if [ "$AC1" = "pass" ]; then wait_health 20 || NOTES+=("после пересоздания health не поднялся за 40с"); fi
|
||||
# контейнер пересоздан — id сменился; без перезапроса docker inspect падает с 'no such object'
|
||||
CID="$(cid)"
|
||||
[ -n "$CID" ] || fatal_env "контейнер awg-dash не найден после пересоздания"
|
||||
CFG_RECREATE="$(compose exec -T awg-dash cat /config/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
|
||||
DATA_RECREATE="$(compose exec -T awg-dash cat /data/.ac2-marker 2>/dev/null | tr -d '\r\n' || true)"
|
||||
|
||||
HOST_CFG="$(tr -d '\r\n' < "${HOST_DIR}/config/.ac2-marker" 2>/dev/null || true)"
|
||||
HOST_DATA="$(tr -d '\r\n' < "${HOST_DIR}/data/.ac2-marker" 2>/dev/null || true)"
|
||||
|
||||
if [ "$CFG_AFTER" = "$MARKER_TEXT" ] && [ "$DATA_AFTER" = "$MARKER_TEXT" ] \
|
||||
&& [ "$CFG_RECREATE" = "$MARKER_TEXT" ] && [ "$DATA_RECREATE" = "$MARKER_TEXT" ]; then
|
||||
AC2="pass"
|
||||
ok "AC-2: /config и /data пережили restart и пересоздание контейнера"
|
||||
if [ "$HOST_CFG" = "$MARKER_TEXT" ] && [ "$HOST_DATA" = "$MARKER_TEXT" ]; then
|
||||
ok "AC-2: маркеры видны на хосте (${HOST_DIR}/{config,data}) — это bind mount, а не слой контейнера"
|
||||
else
|
||||
NOTES+=("маркеры не видны в ${HOST_DIR} — проверьте, что это bind mount, а не volume")
|
||||
fi
|
||||
else
|
||||
AC2="fail"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
bad "AC-2: маркеры не пережили restart/пересоздание (cfg=$CFG_AFTER data=$DATA_AFTER recreate=$CFG_RECREATE/$DATA_RECREATE)"
|
||||
fi
|
||||
else
|
||||
AC2="fail"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
bad "AC-2: не удалось записать маркеры в /config и /data (exec в контейнер не работает)"
|
||||
fi
|
||||
|
||||
# --- монтирования и здоровье -------------------------------------------------
|
||||
CID="$(cid)"
|
||||
[ -n "$CID" ] || fatal_env "контейнер awg-dash не найден в compose project (после перезапуска)"
|
||||
MOUNTS="$(docker inspect -f '{{range .Mounts}}{{.Destination}}<-{{.Source}}{{"\n"}}{{end}}' "$CID" 2>/dev/null || true)"
|
||||
printf '%s\n' "$MOUNTS" | grep -q '/config' || { FAILURES=$((FAILURES + 1)); bad "нет монтирования /config"; }
|
||||
printf '%s\n' "$MOUNTS" | grep -q '/data' || { FAILURES=$((FAILURES + 1)); bad "нет монтирования /data"; }
|
||||
HEALTH_STATUS="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CID" 2>/dev/null || echo unknown)"
|
||||
|
||||
say "контейнер: $(docker inspect -f '{{.Name}} {{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$CID")"
|
||||
say "монтирования:"; printf '%s\n' "$MOUNTS" | sed 's/^/ /'
|
||||
say "образ: $(docker inspect -f '{{.Config.Image}}' "$CID")"
|
||||
|
||||
if [ "$JSON_OUT" != "" ]; then
|
||||
mkdir -p "$(dirname "$JSON_OUT")"
|
||||
cat > "$JSON_OUT" <<EOF
|
||||
{
|
||||
"task": "#21",
|
||||
"host": "$(hostname)",
|
||||
"container": "$CID",
|
||||
"image": "$(docker inspect -f '{{.Config.Image}}' "$CID")",
|
||||
"api_mode": "$API_MODE",
|
||||
"ac1_health_200": "$AC1",
|
||||
"ac2_state_persists_restart": "$AC2",
|
||||
"container_health": "$HEALTH_STATUS",
|
||||
"host_dir": "${HOST_DIR}",
|
||||
"host_port": "${PORT}",
|
||||
"failures": ${FAILURES},
|
||||
"notes": [$(printf '"%s",' "${NOTES[@]:-}" 2>/dev/null | sed 's/,$//')]
|
||||
}
|
||||
EOF
|
||||
ok "отчёт: ${JSON_OUT}"
|
||||
fi
|
||||
|
||||
if [ "$KEEP_STAND" -eq 0 ]; then
|
||||
say "стенд оставлен запущенным (снять: docker compose ${COMPOSE_ARGS[*]} down)"
|
||||
fi
|
||||
|
||||
if [ "$FAILURES" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
[ "$AC1" = "blocked" ] && exit 2
|
||||
exit 0
|
||||
Reference in New Issue
Block a user