Sprint 0 / #21: dev/staging стенд — docker-compose, тома, netns-харнесс #53
@@ -3,8 +3,21 @@ VENV ?= .venv
|
||||
BIN := $(VENV)/bin
|
||||
IMAGE ?= awg-dash:dev
|
||||
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:
|
||||
@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; }
|
||||
docker build -f deploy/Dockerfile -t $(IMAGE) .
|
||||
|
||||
# --- dev/staging стенд (задача #21) ------------------------------------------
|
||||
dev-up:
|
||||
@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:
|
||||
docker compose -f $(COMPOSE_DEV) down
|
||||
AWG_DASH_HOST_DIR=$(HOST_DIR) $(COMPOSE) -f $(COMPOSE_DEV) down
|
||||
|
||||
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 -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
|
||||
|
||||
@@ -6,7 +6,36 @@
|
||||
| `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,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)
|
||||
Reference in New Issue
Block a user