feat(web): DELETE /deckies/{name} single-decky teardown endpoint

The Fleet module had no delete — neither UI nor API — though the engine
capability existed (engine.teardown(decky_id=...), exposed only via
`decnet teardown --id`). Wire it to HTTP.

DELETE /deckies/{name} (admin-gated, 204). Synchronous: a single decky's
compose stop/rm is quick, so it's awaited off-thread rather than the
202+lifecycle path deploy/mutate use for slow builds. The single-decky
teardown never touches the host macvlan interface, so it needs no extra
CAP_NET_ADMIN.

State consistency: engine.teardown removes the containers and the
fleet_deckies row but leaves the decky in decnet-state.json. Left as is, the
reconciler would see "present in JSON, absent from DB" and re-INSERT the row,
resurrecting the decky. So the handler prunes it from both decnet-state.json
and the DB deployment key after teardown; deleting the last decky clears
state entirely (DecnetConfig.deckies has min_length=1).

Route ordering: the dynamic DELETE /deckies/{decky_name} is registered AFTER
the fixed /deckies/* routes (Starlette matches in registration order), so it
no longer shadows DELETE /deckies/files (file-drop).

Tests cover 401/403/404/422, single-delete pruning, and last-decky clear.
This commit is contained in:
2026-06-16 12:07:10 -04:00
parent 8db593a544
commit 0c10869e26
3 changed files with 211 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ from .fleet.api_get_deckies import router as get_deckies_router
from .fleet.api_mutate_decky import router as mutate_decky_router
from .fleet.api_mutate_interval import router as mutate_interval_router
from .fleet.api_deploy_deckies import router as deploy_deckies_router
from .fleet.api_teardown_decky import router as teardown_decky_router
from .fleet.api_lifecycle import router as lifecycle_router
from .stream.api_stream_events import router as stream_router
from .attackers.api_get_attackers import router as attackers_router
@@ -196,6 +197,12 @@ api_router.include_router(topology_router)
api_router.include_router(canary_router)
api_router.include_router(deckies_router)
# Single-decky teardown LAST among /deckies/* routes: its dynamic
# DELETE /deckies/{decky_name} would otherwise shadow the fixed paths
# (e.g. DELETE /deckies/files) since Starlette matches in registration
# order. Fixed paths must be declared before the variable path.
api_router.include_router(teardown_decky_router)
# External webhook subscriptions (SIEM/SOAR egress)
api_router.include_router(webhooks_router)