feat(deckies): generic file drops on fleet + MazeNET deckies

Extracts the docker-exec-with-base64-stdin pattern out of canary/planter
and orchestrator/drivers/ssh into a shared decnet.decky_io package.
Both consumers now delegate; the canary planter test still proves the
contract end-to-end.

Adds POST/DELETE /api/v1/deckies/files for arbitrary file drops.
Container resolution is shared with the canary path: topology_id absent
means fleet (<name>-ssh), present routes through resolve_decky_container
which picks <name>-ssh when the topology decky exposes ssh, else the
topology base container decnet_t_<id8>_<name>.

Path validation rejects relative paths and '..' traversal at the request
model layer.  Bad base64 → 400; unknown topology → 404; decky not in
topology → 422; docker exec failure → 409.
This commit is contained in:
2026-04-28 22:43:34 -04:00
parent 3fe999d706
commit 0bc4b05c73
19 changed files with 1047 additions and 176 deletions

View File

@@ -58,15 +58,30 @@ async def test_traffic_failure_when_banner_missing(monkeypatch):
async def test_file_action_invokes_docker_exec_on_dst(monkeypatch):
captured: list[tuple[list[str], bytes | None]] = []
async def fake_run_with_stdin(argv, stdin_bytes):
captured.append((argv, stdin_bytes))
return 0, "", ""
class _FakeProc:
returncode = 0
async def communicate(self, input=None):
return b"", b""
def kill(self): # pragma: no cover
pass
async def fake_create(*argv, **kw):
captured.append((list(argv), None))
proc = _FakeProc()
orig = proc.communicate
async def communicate(input=None):
captured[-1] = (captured[-1][0], input)
return await orig(None)
proc.communicate = communicate
return proc
# plant_file streams base64 content via stdin to avoid ARG_MAX
# (mirrors decnet.canary.planter; see commit c17b9e0). The test
# patches _run_with_stdin instead of _run because that's the
# codepath FileAction now exercises.
monkeypatch.setattr(ssh_driver, "_run_with_stdin", fake_run_with_stdin)
# (mirrors decnet.canary.planter; see commit c17b9e0). The driver
# now delegates to decky_io.write_file_to_container, which calls
# asyncio.create_subprocess_exec — patch that.
import asyncio as _asyncio
monkeypatch.setattr(_asyncio, "create_subprocess_exec", fake_create)
drv = ssh_driver.SSHDriver()
action = FileAction(
dst_uuid="u2", dst_name="decky-02",
@@ -107,13 +122,21 @@ async def test_run_handles_missing_docker_binary(monkeypatch):
@pytest.mark.asyncio
async def test_plant_file_applies_mtime_via_touch_d(monkeypatch):
from datetime import datetime, timezone
captured: list[tuple[list[str], bytes | None]] = []
captured: list[list[str]] = []
async def fake_run_with_stdin(argv, stdin_bytes):
captured.append((argv, stdin_bytes))
return 0, "", ""
class _FakeProc:
returncode = 0
async def communicate(self, input=None):
return b"", b""
def kill(self): # pragma: no cover
pass
monkeypatch.setattr(ssh_driver, "_run_with_stdin", fake_run_with_stdin)
async def fake_create(*argv, **kw):
captured.append(list(argv))
return _FakeProc()
import asyncio as _asyncio
monkeypatch.setattr(_asyncio, "create_subprocess_exec", fake_create)
drv = ssh_driver.SSHDriver()
mtime = datetime(2026, 4, 20, 11, 30, 0, tzinfo=timezone.utc)
result = await drv.plant_file(
@@ -121,7 +144,7 @@ async def test_plant_file_applies_mtime_via_touch_d(monkeypatch):
mode=0o644, mtime=mtime,
)
assert result.success is True
sh_cmd = captured[0][0][6]
sh_cmd = captured[0][6]
# Backdated mtime appears in the touch -d argument.
assert "touch -d '2026-04-20 11:30:00 UTC'" in sh_cmd
assert "chmod 644" in sh_cmd