fix(engine): teardown(decky_id=...) built malformed service names
The nested list-comp `[f"{id}-{svc}" for svc in [d.services for d ...]]`
iterated over a list of lists, so `svc` was the whole services list and
f-string stringified it -> `decky3-['sip']`. docker compose saw "no such
service" and the per-decky teardown failed 500.
Flatten: find the matching decky once, then iterate its services. Noop
early on unknown decky_id and on empty service lists. Regression test
asserts the emitted compose args have no '[' or quote characters.
This commit is contained in:
@@ -195,10 +195,14 @@ def teardown(decky_id: str | None = None) -> None:
|
|||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
if decky_id:
|
if decky_id:
|
||||||
svc_names = [f"{decky_id}-{svc}" for svc in [d.services for d in config.deckies if d.name == decky_id]]
|
decky = next((d for d in config.deckies if d.name == decky_id), None)
|
||||||
if not svc_names:
|
if decky is None:
|
||||||
console.print(f"[red]Decky '{decky_id}' not found in current deployment.[/]")
|
console.print(f"[red]Decky '{decky_id}' not found in current deployment.[/]")
|
||||||
return
|
return
|
||||||
|
svc_names = [f"{decky_id}-{svc}" for svc in decky.services]
|
||||||
|
if not svc_names:
|
||||||
|
log.warning("teardown: decky %s has no services to stop", decky_id)
|
||||||
|
return
|
||||||
_compose("stop", *svc_names, compose_file=compose_path)
|
_compose("stop", *svc_names, compose_file=compose_path)
|
||||||
_compose("rm", "-f", *svc_names, compose_file=compose_path)
|
_compose("rm", "-f", *svc_names, compose_file=compose_path)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -268,6 +268,43 @@ class TestTeardown:
|
|||||||
teardown()
|
teardown()
|
||||||
mock_td_ipvlan.assert_called_once()
|
mock_td_ipvlan.assert_called_once()
|
||||||
|
|
||||||
|
@patch("decnet.engine.deployer._compose")
|
||||||
|
@patch("decnet.engine.deployer.docker.from_env")
|
||||||
|
@patch("decnet.engine.deployer.load_state")
|
||||||
|
def test_single_decky_emits_flat_service_names(
|
||||||
|
self, mock_load, mock_docker, mock_compose,
|
||||||
|
):
|
||||||
|
"""Regression: teardown(decky_id=...) must iterate the matched decky's
|
||||||
|
services, not stringify the services list itself. The old nested
|
||||||
|
comprehension produced `decky3-['sip']` and docker compose choked."""
|
||||||
|
config = _config(deckies=[
|
||||||
|
_decky(name="decky3", ip="192.168.1.13", services=["sip", "ssh"]),
|
||||||
|
_decky(name="decky4", ip="192.168.1.14", services=["http"]),
|
||||||
|
])
|
||||||
|
mock_load.return_value = (config, Path("test.yml"))
|
||||||
|
from decnet.engine.deployer import teardown
|
||||||
|
teardown(decky_id="decky3")
|
||||||
|
|
||||||
|
# stop + rm, each called with the flat per-service names
|
||||||
|
assert mock_compose.call_count == 2
|
||||||
|
for call in mock_compose.call_args_list:
|
||||||
|
args = call.args
|
||||||
|
svc_names = [a for a in args if a.startswith("decky3-")]
|
||||||
|
assert svc_names == ["decky3-sip", "decky3-ssh"], svc_names
|
||||||
|
for name in svc_names:
|
||||||
|
assert "[" not in name and "'" not in name
|
||||||
|
|
||||||
|
@patch("decnet.engine.deployer._compose")
|
||||||
|
@patch("decnet.engine.deployer.docker.from_env")
|
||||||
|
@patch("decnet.engine.deployer.load_state")
|
||||||
|
def test_unknown_decky_id_is_noop(
|
||||||
|
self, mock_load, mock_docker, mock_compose,
|
||||||
|
):
|
||||||
|
mock_load.return_value = (_config(), Path("test.yml"))
|
||||||
|
from decnet.engine.deployer import teardown
|
||||||
|
teardown(decky_id="does-not-exist")
|
||||||
|
mock_compose.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
# ── status ────────────────────────────────────────────────────────────────────
|
# ── status ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user