diff --git a/decnet/engine/deployer.py b/decnet/engine/deployer.py index fe60a6fb..884c18ca 100644 --- a/decnet/engine/deployer.py +++ b/decnet/engine/deployer.py @@ -5,8 +5,10 @@ Deploy, teardown, and status via Docker SDK + subprocess docker compose. import asyncio import json +import os import shutil import subprocess # nosec B404 +import tempfile import time from pathlib import Path from typing import cast @@ -833,7 +835,16 @@ def _teardown_order(lans: list[dict]) -> list[str]: def _topology_compose_path(topology_id: str) -> Path: - return Path(f"decnet-topology-{topology_id[:8]}-compose.yml") + # Anchor to a stable absolute dir so write and teardown agree + # regardless of process CWD — a CWD-relative path littered the + # install tree and let teardown's unlink() miss orphaned files. + base = Path(os.environ.get("DECNET_RUN_DIR", "/var/lib/decnet/topologies")) + try: + base.mkdir(parents=True, exist_ok=True) + except OSError: + base = Path(tempfile.gettempdir()) / "decnet-topologies" + base.mkdir(parents=True, exist_ok=True) + return base / f"decnet-topology-{topology_id[:8]}-compose.yml" def _topology_compose_project(topology_id: str) -> str: diff --git a/tests/topology/test_deploy.py b/tests/topology/test_deploy.py index 44cb2272..c95e171d 100644 --- a/tests/topology/test_deploy.py +++ b/tests/topology/test_deploy.py @@ -51,7 +51,7 @@ async def repo(tmp_path): @pytest.mark.anyio async def test_dry_run_writes_compose_and_preserves_pending(repo, tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) + monkeypatch.setenv("DECNET_RUN_DIR", str(tmp_path)) plan = generate(_cfg()) tid = await persist(repo, plan) @@ -235,3 +235,22 @@ async def test_deploy_and_teardown_against_real_docker(repo, tmp_path, monkeypat p.unlink() # Sanity: Path roundtrip still resolvable assert isinstance(Path(str(p)), Path) + + +def test_compose_path_is_absolute_and_cwd_independent(tmp_path, monkeypatch): + """Regression: a CWD-relative compose path littered the install dir and + let teardown's unlink() miss orphans. Path must be absolute and stable + across CWD changes so write and teardown always agree.""" + monkeypatch.setenv("DECNET_RUN_DIR", str(tmp_path)) + tid = "abcdef1234567890" + + monkeypatch.chdir(tmp_path) + p1 = _topology_compose_path(tid) + sub = tmp_path / "elsewhere" + sub.mkdir() + monkeypatch.chdir(sub) + p2 = _topology_compose_path(tid) + + assert p1.is_absolute() + assert p1 == p2, "compose path must not depend on process CWD" + assert p1.parent == tmp_path