feat(canary): seed baseline canaries on MazeNET deckies

Topology deploys now plant the configured canary baseline set on every
decky in the topology, mirroring the fleet-deploy hook. Containers are
resolved via resolve_topology_container — <decky>-ssh when the decky
exposes an ssh service, else the topology base container
decnet_t_<id8>_<decky>.

The planter's plant/revoke/seed_baseline grow an optional container=
kwarg; default preserves the fleet <name>-ssh resolution.
This commit is contained in:
2026-04-28 22:30:11 -04:00
parent 04b0637c24
commit 5802de1f86
3 changed files with 176 additions and 2 deletions

View File

@@ -233,6 +233,98 @@ async def test_seed_baseline_skips_unknown_generator(repo: SQLiteRepository, mon
assert {r["generator"] for r in rows} == {"env_file"}
@pytest.mark.asyncio
async def test_plant_honours_explicit_container_override(repo: SQLiteRepository) -> None:
"""``container=`` lets MazeNET callers target a non-``<name>-ssh`` container."""
await repo.create_canary_token({
"uuid": "tok-c", "kind": "http", "decky_name": "web1",
"generator": "env_file", "placement_path": "/x",
"callback_token": "slugC", "secret_seed": "s", "created_by": "u1",
})
art = CanaryArtifact(path="/x", content=b"y", generator="env_file")
patcher, captured, _stdin = _patch_subprocess(rc=0)
with patcher:
ok, _err = await planter.plant(
"web1", art, token_uuid="tok-c", repo=repo,
container="decnet_t_abc12345_web1",
)
assert ok is True
# docker exec -i <override-container> ...
assert captured[0][3] == "decnet_t_abc12345_web1"
def test_resolve_topology_container_prefers_ssh_service() -> None:
name = planter.resolve_topology_container(
"abc123def456", "web1", services=["ssh", "http"],
)
assert name == "web1-ssh"
def test_resolve_topology_container_falls_back_to_base() -> None:
name = planter.resolve_topology_container(
"abc123def456789", "router", services=["dns"],
)
# decnet_t_<id8>_<decky_name>; matches topology.compose._container_name.
assert name == "decnet_t_abc123de_router"
@pytest.mark.asyncio
async def test_seed_baseline_topology_iterates_deckies_and_resolves_container(
repo: SQLiteRepository, monkeypatch
) -> None:
"""Topology seed: ssh-bearing decky → ``<name>-ssh``; bare decky → base."""
monkeypatch.setenv("DECNET_CANARY_BASELINE", "env_file")
topo_id = "abcdef0123456789"
async def _fake_hydrate(_repo, _topo_id):
assert _topo_id == topo_id
return {
"topology": {"id": topo_id},
"lans": [],
"deckies": [
{
"uuid": "u1", "name": "web1",
"decky_config": {"name": "web1"},
"services": ["ssh", "http"],
},
{
"uuid": "u2", "name": "router",
"decky_config": {"name": "router"},
"services": ["dns"],
},
],
"edges": [],
}
import decnet.canary.planter as _planter_mod
monkeypatch.setattr(
"decnet.topology.persistence.hydrate", _fake_hydrate,
)
patcher, captured, _stdin = _patch_subprocess(rc=0)
with patcher:
rows = await _planter_mod.seed_baseline_topology(repo, topo_id)
# One token per decky × one generator in the baseline.
assert {r["decky_name"] for r in rows} == {"web1", "router"}
# docker exec -i <container> ... — captured argv index 3 is container.
containers = sorted(argv[3] for argv in captured)
assert containers == ["decnet_t_abcdef01_router", "web1-ssh"]
@pytest.mark.asyncio
async def test_seed_baseline_topology_returns_empty_for_missing_topology(
repo: SQLiteRepository, monkeypatch
) -> None:
async def _none_hydrate(_repo, _topo_id):
return None
monkeypatch.setattr(
"decnet.topology.persistence.hydrate", _none_hydrate,
)
rows = await planter.seed_baseline_topology(repo, "missing-id")
assert rows == []
@pytest.mark.asyncio
async def test_seed_baseline_marks_failed_when_docker_errors(
repo: SQLiteRepository, monkeypatch