Sprint 0 / #21: dev/staging стенд — docker-compose, тома, netns-харнесс #53

Merged
embeddevio merged 30 commits from feat/21-dev-stand into main 2026-10-28 22:12:03 +03:00
3 changed files with 151 additions and 5 deletions
Showing only changes of commit 59c3ad4918 - Show all commits
+54 -5
View File
@@ -3,8 +3,21 @@ VENV ?= .venv
BIN := $(VENV)/bin BIN := $(VENV)/bin
IMAGE ?= awg-dash:dev IMAGE ?= awg-dash:dev
COMPOSE_DEV := deploy/docker-compose.dev.yml COMPOSE_DEV := deploy/docker-compose.dev.yml
COMPOSE_TEST := deploy/docker-compose.test.yml
COMPOSE_STUB := deploy/docker-compose.stub.yml
VERIFY := deploy/verify-dev-stand.sh
COMPOSE ?= docker compose
# Состояние dev-стенда: bind mount для /config и /data (AC-2 задачи #21).
HOST_DIR ?= /opt/awg-dash-dev
# Хост-порт стенда. На dev-хосте 8080 занят другим сервисом — по умолчанию 8180;
# внутри контейнера приложение всегда на 8080.
DASH_PORT ?= 8180
NETNS_ARGS ?=
VERIFY_ARGS ?=
MANIFEST_ARGS ?=
.PHONY: help venv lint fmt test build dev-up dev-down dev-logs test-netns .PHONY: help venv lint fmt test build dev-up dev-up-stub dev-down dev-logs dev-verify \
dev-verify-stub dev-reset test-netns test-netns-keep test-netns-ci test-manifests lint-shell
help: help:
@grep -E '^[a-zA-Z_-]+:' $(MAKEFILE_LIST) | sed 's/:.*//' | sort -u @grep -E '^[a-zA-Z_-]+:' $(MAKEFILE_LIST) | sed 's/:.*//' | sort -u
@@ -29,16 +42,52 @@ build:
@test -f deploy/Dockerfile || { echo 'deploy/Dockerfile ещё нет — задача #18'; exit 1; } @test -f deploy/Dockerfile || { echo 'deploy/Dockerfile ещё нет — задача #18'; exit 1; }
docker build -f deploy/Dockerfile -t $(IMAGE) . docker build -f deploy/Dockerfile -t $(IMAGE) .
# --- dev/staging стенд (задача #21) ------------------------------------------
dev-up: dev-up:
@test -f $(COMPOSE_DEV) || { echo '$(COMPOSE_DEV) ещё нет — задача #21'; exit 1; } @test -f $(COMPOSE_DEV) || { echo '$(COMPOSE_DEV) ещё нет — задача #21'; exit 1; }
docker compose -f $(COMPOSE_DEV) up -d --build @command -v docker >/dev/null 2>&1 || { echo 'нет docker: dev-стенд поднимается на Docker-хосте (см. deploy/dev-stand.md)'; exit 1; }
AWG_DASH_PORT=$(DASH_PORT) AWG_DASH_HOST_DIR=$(HOST_DIR) $(COMPOSE) -f $(COMPOSE_DEV) up -d --build
# Health-заглушка вместо API до мержа #31 — только для проверки плумбинга стенда.
dev-up-stub:
@test -f $(COMPOSE_STUB) || { echo '$(COMPOSE_STUB) ещё нет — задача #21'; exit 1; }
AWG_DASH_PORT=$(DASH_PORT) AWG_DASH_HOST_DIR=$(HOST_DIR) $(COMPOSE) -f $(COMPOSE_DEV) -f $(COMPOSE_STUB) up -d --build
dev-down: dev-down:
docker compose -f $(COMPOSE_DEV) down AWG_DASH_HOST_DIR=$(HOST_DIR) $(COMPOSE) -f $(COMPOSE_DEV) down
dev-logs: dev-logs:
docker compose -f $(COMPOSE_DEV) logs -f --tail=100 AWG_DASH_HOST_DIR=$(HOST_DIR) $(COMPOSE) -f $(COMPOSE_DEV) logs -f --tail=100
# Проверка AC #21 на Docker-хосте: health 200 + состояние переживает restart.
dev-verify:
AWG_DASH_PORT=$(DASH_PORT) AWG_DASH_HOST_DIR=$(HOST_DIR) bash $(VERIFY) $(VERIFY_ARGS)
dev-verify-stub:
AWG_DASH_PORT=$(DASH_PORT) AWG_DASH_HOST_DIR=$(HOST_DIR) bash $(VERIFY) --stub $(VERIFY_ARGS)
# Останавливает стенд; каталоги состояния сохраняются (удалять вручную).
dev-reset:
AWG_DASH_HOST_DIR=$(HOST_DIR) $(COMPOSE) -f $(COMPOSE_DEV) down --remove-orphans
@echo "каталоги состояния сохранены: $(HOST_DIR)/config, $(HOST_DIR)/data (удалять вручную)"
# --- netns-стенд (задача #21) ------------------------------------------------
test-netns: test-netns:
@test -d tests/netns || { echo 'tests/netns ещё нет — задача #21'; exit 1; } @test -d tests/netns || { echo 'tests/netns ещё нет — задача #21'; exit 1; }
bash tests/netns/run.sh bash tests/netns/run.sh $(NETNS_ARGS)
# То же, но топология и логи остаются для отладки (снести: tests/netns/topology.sh down).
test-netns-keep:
bash tests/netns/run.sh --keep-topology --keep-logs $(NETNS_ARGS)
# Тот же стенд в привилегированном контейнере — путь для CI (#17).
test-netns-ci:
AWG_DASH_IMAGE=$(IMAGE) $(COMPOSE) -f $(COMPOSE_TEST) run --rm netns
# --- статические проверки манифестов и скриптов ------------------------------
test-manifests:
$(PY) tests/deploy/validate_compose.py $(MANIFEST_ARGS)
lint-shell:
@command -v shellcheck >/dev/null 2>&1 || { echo 'shellcheck не установлен (apt-get install shellcheck)'; exit 1; }
shellcheck -x -S style tests/netns/*.sh deploy/*.sh
+29
View File
@@ -6,7 +6,36 @@
| `entrypoint.sh` | супервизор: `awg-agent` (root) + `awg-api` | #18 | | `entrypoint.sh` | супервизор: `awg-agent` (root) + `awg-api` | #18 |
| `docker-compose.dev.yml` | dev-стенд: порт 8080, тома `/config` `/data`, healthcheck | #21 | | `docker-compose.dev.yml` | dev-стенд: порт 8080, тома `/config` `/data`, healthcheck | #21 |
| `docker-compose.test.yml` | стенд для тестов | #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 | | `grafana/` | дашборд JSON (опциональный профиль monitoring) | #46 |
| `awg0.conf.example` | пример клиентского конфига | #22 | | `awg0.conf.example` | пример клиентского конфига | #22 |
Запуск по ТЗ (после релиза): `--network=host`, `--device=/dev/net/tun`, `--cap-add=NET_ADMIN --cap-add=SYS_MODULE`, `--restart always`. Запуск по ТЗ (после релиза): `--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).
+68
View File
@@ -0,0 +1,68 @@
"""Тесты статических инвариантов compose-файлов стенда (задача #21).
Проверяют то, что иначе выяснилось бы только на Docker-хосте: порт 8080,
монтирования /config и /data, healthcheck, capabilities, privileged netns-раннер.
Работают без Docker — только PyYAML (см. requirements-dev.txt).
"""
from __future__ import annotations
import pathlib
import unittest
import validate_compose
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parents[1]
@unittest.skipUnless(validate_compose.yaml is not None, "нужен PyYAML (make venv)")
class ComposeManifestsTest(unittest.TestCase):
def test_dev_compose_matches_acceptance_criteria(self) -> None:
errors, _ = validate_compose.validate(require_dockerfile=False)
self.assertEqual([], errors, "compose-файлы стенда нарушают инварианты AC #21")
def test_dev_compose_publishes_app_port_to_host(self) -> None:
doc = validate_compose.load(validate_compose.DEV)
ports = " ".join(str(p) for p in doc["services"]["awg-dash"]["ports"])
self.assertIn(":8080", ports, "наружу должен публиковаться контейнерный порт приложения 8080")
def test_dev_compose_mounts_state_directories(self) -> None:
doc = validate_compose.load(validate_compose.DEV)
targets = validate_compose._mount_targets(doc["services"]["awg-dash"])
self.assertIn("/config", targets)
self.assertIn("/data", targets)
def test_healthcheck_hits_api_v1_health(self) -> None:
doc = validate_compose.load(validate_compose.DEV)
test_cmd = validate_compose.jsonish(doc["services"]["awg-dash"]["healthcheck"]["test"])
self.assertIn("/api/v1/health", test_cmd)
def test_test_compose_runs_netns_harness_privileged(self) -> None:
doc = validate_compose.load(validate_compose.TEST)
svc = doc["services"]["netns"]
self.assertTrue(svc["privileged"])
self.assertIn("tests/netns/run.sh", " ".join(svc["entrypoint"]))
def test_stub_override_is_labelled_dev_only(self) -> None:
text = validate_compose.STUB.read_text(encoding="utf-8")
self.assertIn("api_stub.py", text)
self.assertIn("DEV", text.upper())
def test_netns_harness_entrypoints_exist(self) -> None:
for rel in (
"tests/netns/run.sh",
"tests/netns/topology.sh",
"tests/netns/lib.sh",
"tests/netns/probe.py",
"tests/netns/mock_wg_endpoint.py",
"tests/netns/leak_sink.py",
"deploy/api_stub.py",
"deploy/verify-dev-stand.sh",
):
with self.subTest(rel=rel):
self.assertTrue((ROOT / rel).exists(), f"нет файла {rel}")
if __name__ == "__main__":
unittest.main(verbosity=2)