diff --git a/decnet/web/collector.py b/decnet/web/collector.py index bd92434..1eb5119 100644 --- a/decnet/web/collector.py +++ b/decnet/web/collector.py @@ -83,43 +83,34 @@ def parse_rfc5424(line: str) -> Optional[dict[str, Any]]: # ─── Container helpers ──────────────────────────────────────────────────────── -def _is_decnet_service_labels(labels: dict) -> bool: +def _load_service_container_names() -> set[str]: """ - Return True if the Compose labels indicate a DECNET service container. - - Discriminator: base containers have no depends_on (they own the IP); - service containers all declare depends_on pointing at their base. - Both sets carry com.docker.compose.project=decnet. + Return the exact set of service container names from decnet-state.json. + Format: {decky_name}-{service_name}, e.g. 'omega-decky-smtp'. + Returns an empty set if no state file exists. """ - if labels.get("com.docker.compose.project") != "decnet": - return False - return bool(labels.get("com.docker.compose.depends_on", "").strip()) + from decnet.config import load_state + state = load_state() + if state is None: + return set() + config, _ = state + names: set[str] = set() + for decky in config.deckies: + for svc in decky.services: + names.add(f"{decky.name}-{svc.replace('_', '-')}") + return names def is_service_container(container) -> bool: - """ - Return True for DECNET service containers. - - Accepts either a Docker SDK container object or a plain name string - (legacy path — falls back to label-free heuristic when only a name - is available, which is always less reliable). - """ - if isinstance(container, str): - # Called with a name only (e.g. from event stream before full inspect). - # Best-effort: a base container name has no service suffix, so it won't - # contain a hyphen after the decky name. We can't be certain without - # labels, so this path is only kept for the event fast-path and is - # superseded by the label check in the initial scan. - name = container.lstrip("/") - # Filter out anything not from our project (best effort via name) - return "-" in name # will be re-checked via labels on _spawn - labels = container.labels or {} - return _is_decnet_service_labels(labels) + """Return True if this Docker container is a known DECNET service container.""" + name = (container if isinstance(container, str) else container.name).lstrip("/") + return name in _load_service_container_names() def is_service_event(attrs: dict) -> bool: - """Return True if a Docker event's Actor.Attributes are for a DECNET service container.""" - return _is_decnet_service_labels(attrs) + """Return True if a Docker start event is for a known DECNET service container.""" + name = attrs.get("name", "").lstrip("/") + return name in _load_service_container_names() # ─── Blocking stream worker (runs in a thread) ──────────────────────────────── diff --git a/tests/test_collector.py b/tests/test_collector.py index 7a94930..5157f2e 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -2,29 +2,14 @@ import json from types import SimpleNamespace +from unittest.mock import patch from decnet.web.collector import parse_rfc5424, is_service_container, is_service_event - -def _make_container(project="decnet", depends_on="omega-decky:service_started:false"): - """Return a mock container object with Compose labels.""" - return SimpleNamespace( - name="omega-decky-http", - labels={ - "com.docker.compose.project": project, - "com.docker.compose.depends_on": depends_on, - }, - ) +_KNOWN_NAMES = {"omega-decky-http", "omega-decky-smtp", "relay-decky-ftp"} -def _make_base_container(): - """Return a mock base container (no depends_on).""" - return SimpleNamespace( - name="omega-decky", - labels={ - "com.docker.compose.project": "decnet", - "com.docker.compose.depends_on": "", - }, - ) +def _make_container(name="omega-decky-http"): + return SimpleNamespace(name=name) class TestParseRfc5424: @@ -105,42 +90,43 @@ class TestParseRfc5424: class TestIsServiceContainer: - def test_service_container_returns_true(self): - assert is_service_container(_make_container()) is True + def test_known_container_returns_true(self): + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_container(_make_container("omega-decky-http")) is True + assert is_service_container(_make_container("omega-decky-smtp")) is True + assert is_service_container(_make_container("relay-decky-ftp")) is True def test_base_container_returns_false(self): - assert is_service_container(_make_base_container()) is False + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_container(_make_container("omega-decky")) is False - def test_different_decky_name_styles(self): - # omega-decky style (ini section name) - assert is_service_container(_make_container(depends_on="omega-decky:service_started:false")) is True - # relay-decky style - assert is_service_container(_make_container(depends_on="relay-decky:service_started:false")) is True + def test_unrelated_container_returns_false(self): + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_container(_make_container("nginx")) is False - def test_wrong_project_returns_false(self): - assert is_service_container(_make_container(project="someother")) is False + def test_strips_leading_slash(self): + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_container(_make_container("/omega-decky-http")) is True + assert is_service_container(_make_container("/omega-decky")) is False - def test_no_labels_returns_false(self): - c = SimpleNamespace(name="nginx", labels={}) - assert is_service_container(c) is False + def test_no_state_returns_false(self): + with patch("decnet.web.collector._load_service_container_names", return_value=set()): + assert is_service_container(_make_container("omega-decky-http")) is False class TestIsServiceEvent: - def _make_attrs(self, project="decnet", depends_on="omega-decky:service_started:false"): - return { - "com.docker.compose.project": project, - "com.docker.compose.depends_on": depends_on, - "name": "omega-decky-smtp", - } - - def test_service_event_returns_true(self): - assert is_service_event(self._make_attrs()) is True + def test_known_service_event_returns_true(self): + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_event({"name": "omega-decky-smtp"}) is True def test_base_event_returns_false(self): - assert is_service_event(self._make_attrs(depends_on="")) is False - - def test_wrong_project_returns_false(self): - assert is_service_event(self._make_attrs(project="other")) is False + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_event({"name": "omega-decky"}) is False def test_unrelated_event_returns_false(self): - assert is_service_event({"name": "nginx"}) is False + with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES): + assert is_service_event({"name": "nginx"}) is False + + def test_no_state_returns_false(self): + with patch("decnet.web.collector._load_service_container_names", return_value=set()): + assert is_service_event({"name": "omega-decky-smtp"}) is False