refactor(collector): use state file for container detection, drop label heuristics
_load_service_container_names() reads decnet-state.json and builds the
exact set of expected container names ({decky}-{service}). is_service_container()
and is_service_event() do a direct set lookup — no regex, no label
inspection, no heuristics.
This commit is contained in:
@@ -83,43 +83,34 @@ def parse_rfc5424(line: str) -> Optional[dict[str, Any]]:
|
|||||||
|
|
||||||
# ─── Container helpers ────────────────────────────────────────────────────────
|
# ─── 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.
|
Return the exact set of service container names from decnet-state.json.
|
||||||
|
Format: {decky_name}-{service_name}, e.g. 'omega-decky-smtp'.
|
||||||
Discriminator: base containers have no depends_on (they own the IP);
|
Returns an empty set if no state file exists.
|
||||||
service containers all declare depends_on pointing at their base.
|
|
||||||
Both sets carry com.docker.compose.project=decnet.
|
|
||||||
"""
|
"""
|
||||||
if labels.get("com.docker.compose.project") != "decnet":
|
from decnet.config import load_state
|
||||||
return False
|
state = load_state()
|
||||||
return bool(labels.get("com.docker.compose.depends_on", "").strip())
|
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:
|
def is_service_container(container) -> bool:
|
||||||
"""
|
"""Return True if this Docker container is a known DECNET service container."""
|
||||||
Return True for DECNET service containers.
|
name = (container if isinstance(container, str) else container.name).lstrip("/")
|
||||||
|
return name in _load_service_container_names()
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def is_service_event(attrs: dict) -> bool:
|
def is_service_event(attrs: dict) -> bool:
|
||||||
"""Return True if a Docker event's Actor.Attributes are for a DECNET service container."""
|
"""Return True if a Docker start event is for a known DECNET service container."""
|
||||||
return _is_decnet_service_labels(attrs)
|
name = attrs.get("name", "").lstrip("/")
|
||||||
|
return name in _load_service_container_names()
|
||||||
|
|
||||||
|
|
||||||
# ─── Blocking stream worker (runs in a thread) ────────────────────────────────
|
# ─── Blocking stream worker (runs in a thread) ────────────────────────────────
|
||||||
|
|||||||
@@ -2,29 +2,14 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
from decnet.web.collector import parse_rfc5424, is_service_container, is_service_event
|
from decnet.web.collector import parse_rfc5424, is_service_container, is_service_event
|
||||||
|
|
||||||
|
_KNOWN_NAMES = {"omega-decky-http", "omega-decky-smtp", "relay-decky-ftp"}
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _make_base_container():
|
def _make_container(name="omega-decky-http"):
|
||||||
"""Return a mock base container (no depends_on)."""
|
return SimpleNamespace(name=name)
|
||||||
return SimpleNamespace(
|
|
||||||
name="omega-decky",
|
|
||||||
labels={
|
|
||||||
"com.docker.compose.project": "decnet",
|
|
||||||
"com.docker.compose.depends_on": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestParseRfc5424:
|
class TestParseRfc5424:
|
||||||
@@ -105,42 +90,43 @@ class TestParseRfc5424:
|
|||||||
|
|
||||||
|
|
||||||
class TestIsServiceContainer:
|
class TestIsServiceContainer:
|
||||||
def test_service_container_returns_true(self):
|
def test_known_container_returns_true(self):
|
||||||
assert is_service_container(_make_container()) is True
|
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):
|
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):
|
def test_unrelated_container_returns_false(self):
|
||||||
# omega-decky style (ini section name)
|
with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES):
|
||||||
assert is_service_container(_make_container(depends_on="omega-decky:service_started:false")) is True
|
assert is_service_container(_make_container("nginx")) is False
|
||||||
# relay-decky style
|
|
||||||
assert is_service_container(_make_container(depends_on="relay-decky:service_started:false")) is True
|
|
||||||
|
|
||||||
def test_wrong_project_returns_false(self):
|
def test_strips_leading_slash(self):
|
||||||
assert is_service_container(_make_container(project="someother")) is False
|
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):
|
def test_no_state_returns_false(self):
|
||||||
c = SimpleNamespace(name="nginx", labels={})
|
with patch("decnet.web.collector._load_service_container_names", return_value=set()):
|
||||||
assert is_service_container(c) is False
|
assert is_service_container(_make_container("omega-decky-http")) is False
|
||||||
|
|
||||||
|
|
||||||
class TestIsServiceEvent:
|
class TestIsServiceEvent:
|
||||||
def _make_attrs(self, project="decnet", depends_on="omega-decky:service_started:false"):
|
def test_known_service_event_returns_true(self):
|
||||||
return {
|
with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES):
|
||||||
"com.docker.compose.project": project,
|
assert is_service_event({"name": "omega-decky-smtp"}) is True
|
||||||
"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_base_event_returns_false(self):
|
def test_base_event_returns_false(self):
|
||||||
assert is_service_event(self._make_attrs(depends_on="")) 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_wrong_project_returns_false(self):
|
|
||||||
assert is_service_event(self._make_attrs(project="other")) is False
|
|
||||||
|
|
||||||
def test_unrelated_event_returns_false(self):
|
def test_unrelated_event_returns_false(self):
|
||||||
|
with patch("decnet.web.collector._load_service_container_names", return_value=_KNOWN_NAMES):
|
||||||
assert is_service_event({"name": "nginx"}) is False
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user