From c7f2ea128e09b1ed685a4a25d55fce86f50f6cec Mon Sep 17 00:00:00 2001 From: embeddevio Date: Wed, 28 Oct 2026 21:30:33 +0300 Subject: [PATCH] =?UTF-8?q?deploy:=20=D0=B3=D0=B5=D0=B9=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D1=8F=20=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=D0=B0=20=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D1=83=D0=B5=D0=BC=D0=B0=D1=8F=20health-=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BB=D1=83=D1=88=D0=BA=D0=B0=20(#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy/api_stub.py: make_server() выделен из main(), main(argv) — тесты поднимают настоящий сервер на порту 0 (покрытие api_stub.py 0 % -> 98 %). - Makefile: make test-deploy (гейт ≥70 % через tests/deploy/pytest.ini), test-netns-ci прокидывает AWG_NS_EXEC_MODE. Refs #21, Refs #15, Refs #56 --- Makefile | 19 +++++++++++++++++-- deploy/api_stub.py | 21 ++++++++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index d13af0e..638ec38 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,12 @@ DASH_PORT ?= 8180 NETNS_ARGS ?= VERIFY_ARGS ?= MANIFEST_ARGS ?= +# Режим входа в netns внутри тестового контейнера: auto|ip|nsenter (issue #56). +NETNS_EXEC_MODE ?= auto .PHONY: help venv lint fmt test build dev-up dev-up-app 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 + dev-verify-stub dev-reset test-netns test-netns-keep test-netns-ci test-manifests lint-shell \ + test-deploy help: @grep -E '^[a-zA-Z_-]+:' $(MAKEFILE_LIST) | sed 's/:.*//' | sort -u @@ -106,13 +109,25 @@ test-netns-keep: bash tests/netns/run.sh --keep-topology --keep-logs $(NETNS_ARGS) # Тот же стенд в привилегированном контейнере — путь для CI (#17). +# Внутри контейнера `ip netns exec` падает с "mount of /sys failed: Operation +# not permitted" (iproute2 6.x), поэтому харнесс сам переключается на +# `nsenter --net=/var/run/netns/` (issue #56). AWG_NS_EXEC_MODE форсирует +# режим: ip|nsenter|auto. test-netns-ci: - AWG_DASH_IMAGE=$(IMAGE) $(COMPOSE) -f $(COMPOSE_TEST) run --rm netns + AWG_DASH_IMAGE=$(IMAGE) AWG_NS_EXEC_MODE=$(NETNS_EXEC_MODE) $(COMPOSE) -f $(COMPOSE_TEST) run --rm netns # --- статические проверки манифестов и скриптов ------------------------------ test-manifests: $(PY) tests/deploy/validate_compose.py $(MANIFEST_ARGS) +# Тесты стенда с покрытием: гейт DoD «тесты ≥70 %» для #21/#56. +# Отдельный tests/deploy/pytest.ini обязателен: без него `pytest tests/deploy` +# наследует addopts корневого pyproject.toml (--cov=awg_agent --cov=awg_api), +# покрытие выходит 0.00 % и RC=1 — гейт приходилось гонять с --no-cov. +test-deploy: + @test -d tests/deploy || { echo 'tests/deploy ещё нет — задача #21'; exit 1; } + $(PY) -m pytest tests/deploy -c tests/deploy/pytest.ini + 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 diff --git a/deploy/api_stub.py b/deploy/api_stub.py index 848922d..9366311 100644 --- a/deploy/api_stub.py +++ b/deploy/api_stub.py @@ -25,6 +25,8 @@ 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" @@ -40,7 +42,7 @@ class Handler(BaseHTTPRequestHandler): def do_GET(self) -> None: # noqa: N802 (HTTP-метод) path = self.path.split("?", 1)[0] - if path in ("/api/v1/health", "/healthz"): + if path in HEALTH_PATHS: self._json( 200, { @@ -56,7 +58,7 @@ class Handler(BaseHTTPRequestHandler): 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 ("/api/v1/health", "/healthz") else 404) + self.send_response(200 if self.path.split("?", 1)[0] in HEALTH_PATHS else 404) self.send_header("Content-Length", "0") self.end_headers() @@ -64,19 +66,28 @@ class Handler(BaseHTTPRequestHandler): print(f"[api-stub] {self.address_string()} {fmt % args}", flush=True) -def main() -> int: +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() + 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, ) - ThreadingHTTPServer((args.host, args.port), Handler).serve_forever() + make_server(args.host, args.port).serve_forever() return 0