feat(canary): allow custom canaries on MazeNET deckies via API

POST /api/v1/canary/tokens grows an optional topology_id field.  When
present, the server hydrates the topology, validates the named decky is
in it, and resolves the docker container via
planter.resolve_topology_container — <name>-ssh if the decky exposes ssh,
else the topology base container.  Absent ⇒ fleet semantics, unchanged.

The token row gets a nullable topology_id column (no migration helper
per pre-v1 policy).  GET /api/v1/canary/tokens accepts ?topology_id= as
a filter.  DELETE re-resolves the container at revoke time so a
redeployed topology is still reachable.

422 when the named decky isn't in the topology; 404 when the topology
itself doesn't exist.
This commit is contained in:
2026-04-28 22:34:45 -04:00
parent 5802de1f86
commit 3fe999d706
5 changed files with 296 additions and 2 deletions

View File

@@ -122,6 +122,7 @@ class CanaryMixin:
decky_name: Optional[str] = None,
state: Optional[str] = None,
kind: Optional[str] = None,
topology_id: Optional[str] = None,
) -> list[dict[str, Any]]:
async with self._session() as session:
stmt = select(CanaryToken)
@@ -131,6 +132,8 @@ class CanaryMixin:
stmt = stmt.where(CanaryToken.state == state)
if kind is not None:
stmt = stmt.where(CanaryToken.kind == kind)
if topology_id is not None:
stmt = stmt.where(CanaryToken.topology_id == topology_id)
stmt = stmt.order_by(desc(CanaryToken.placed_at))
result = await session.execute(stmt)
return [r.model_dump(mode="json") for r in result.scalars().all()]