Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,26 +153,50 @@ In this mode, no mounts and no extra variables are necessary. The CLI
finds the daemon socket automatically.

drukbox can also run as a container adjacent to the daemon. Docker does
not document this mode; drukbox uses the CLI's own daemon-endpoint
variable, `DOCKER_SANDBOXES_API`. Mount the daemon socket, the `sbx`
binary of the host (then the CLI version and the daemon version always
agree), the CLI auth store, and the workspace root:
not document this mode. Run the container with the uid of the daemon
owner. Mount the `sbx` binary of the host, so that the CLI version and
the daemon version always agree. Mount the sbx directories and the
drukbox directory at the same paths as on the host:

```bash
docker run --rm --network host \
--mount type=bind,src=$HOME/.local/state/sandboxes/sandboxes/sandboxd/sandboxd.sock,dst=/run/sandboxd.sock \
--user "$(id -u):$(id -g)" \
--mount type=bind,src=$(command -v sbx),dst=/usr/local/bin/sbx,readonly \
--mount type=bind,src=$HOME/.config/com.docker.sandboxes,dst=/root/.config/com.docker.sandboxes,readonly \
--mount type=bind,src=$HOME/.drukbox/sbx-workspaces,dst=$HOME/.drukbox/sbx-workspaces \
--env DOCKER_SANDBOXES_API=unix:///run/sandboxd.sock \
--mount type=bind,src=$HOME/.local/state/sandboxes/sandboxes/sandboxd,dst=$HOME/.local/state/sandboxes/sandboxes/sandboxd \
--mount type=bind,src=$HOME/.cache/sandboxes,dst=$HOME/.cache/sandboxes \
--mount type=bind,src=$HOME/.config/com.docker.sandboxes,dst=$HOME/.config/com.docker.sandboxes \
--mount type=bind,src=$HOME/.config/sandboxes,dst=$HOME/.config/sandboxes \
--mount type=bind,src=$HOME/.drukbox,dst=$HOME/.drukbox \
--env XDG_CONFIG_HOME=$HOME/.config \
--env XDG_CACHE_HOME=$HOME/.cache \
--env XDG_STATE_HOME=$HOME/.local/state \
--env DOCKER_SANDBOXES_API=unix://$HOME/.local/state/sandboxes/sandboxes/sandboxd/sandboxd.sock \
--env DOCKER_SBX_WORKSPACE_ROOT=$HOME/.drukbox/sbx-workspaces \
--env-file drukbox.env \
ghcr.io/czpython/drukbox:latest
```

The daemon reads workspace paths on its own filesystem. Thus the
workspace mount must have the same path on the host and in the
container. The janitor and pool containers need the same mounts and
The container has no home directory for the daemon owner. Thus the
`XDG_*` variables point the CLI to the mounted directories. The mounts
have these reasons:

- The daemon reads workspace paths on its own filesystem. Thus the
workspace root must have the same path on the host and in the
container.
- The mount holds the directory of the daemon socket, not the socket
file. A daemon restart makes a new socket, and a file mount keeps the
old one.
- `sbx ssh proxy` finds the socket through `XDG_STATE_HOME` only, and
it ignores `DOCKER_SANDBOXES_API`. The other commands use
`DOCKER_SANDBOXES_API`.
- The CLI reads its feature flags from the cache. Without the cache,
it sees the SSH endpoint of the daemon as off, and the gateway tunnel
fails. `/doctor` reports this failure.
- The auth store and the settings store must be writable. The CLI takes
a lock file in the auth store also for reads, and it writes the
settings store on first use.

The janitor, pool, and gateway containers need the same mounts and
variables.

Callers reach the sandboxes through
Expand Down Expand Up @@ -608,6 +632,6 @@ Docker Sandboxes provider:
| `DOCKER_SBX_WORKSPACE_ROOT` | `~/.drukbox/sbx-workspaces` | Directory with one temporary workspace for each sandbox, and a `secrets` directory with the value files sbx reads. The path must be the same for drukbox and for the daemon. |

The published image does not contain the `sbx` CLI. Mount the binary and
the auth store of the host, as
the sbx directories of the host, as
[Local microVMs with Docker Sandboxes](#local-microvms-with-docker-sandboxes)
shows. Set `DOCKER_SANDBOXES_API` to the mounted daemon socket.
shows.
8 changes: 8 additions & 0 deletions src/providers/docker_sbx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ async def sandbox_count(self) -> int:
f"sbx ls returned an unreadable sandbox list: {output.strip()!r}"
) from error

async def check_ssh_endpoint(self) -> None:
"""The gateway tunnel needs the daemon's SSH endpoint. The CLI reads the
endpoint's feature flag from its cache, so a container without the
cache sees it as off. The daemon sends its banner for any name."""
output = await self._run("ssh", "proxy", "drukbox-diagnose.sbx")
if not output.startswith("SSH-"):
raise DockerSbxTransportError(f"sbx ssh proxy sent no SSH banner: {output.strip()!r}")

async def _run(self, *args: str, stdin: str | None = None) -> str:
try:
process = await asyncio.create_subprocess_exec(
Expand Down
4 changes: 3 additions & 1 deletion src/providers/docker_sbx/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ async def delete_template_image(self, image: str) -> None:
raise ProviderTransportError(str(exc)) from exc

async def diagnose(self) -> str:
return f"sandboxd reachable, {await self.api.sandbox_count()} sandbox(es)"
count = await self.api.sandbox_count()
await self.api.check_ssh_endpoint()
return f"sandboxd reachable, {count} sandbox(es), SSH endpoint ready"

async def aclose(self) -> None:
await self.docker.aclose()
Expand Down
34 changes: 34 additions & 0 deletions src/providers/docker_sbx/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,40 @@ async def test_sandbox_count_treats_a_null_list_as_empty(monkeypatch):
assert await SbxCLI().sandbox_count() == 0


@pytest.mark.asyncio
async def test_check_ssh_endpoint_accepts_the_daemon_banner(monkeypatch):
captured: dict = {}

async def fake_exec(*args, **kwargs):
captured["args"] = args
return _process(stdout=b"SSH-2.0-Go\r\n")

monkeypatch.setattr("providers.docker_sbx.api.asyncio.create_subprocess_exec", fake_exec)

await SbxCLI().check_ssh_endpoint()

assert captured["args"][:3] == ("sbx", "ssh", "proxy")


@pytest.mark.asyncio
async def test_check_ssh_endpoint_reports_an_endpoint_the_cli_sees_as_off(monkeypatch):
disabled = b"error: the sandboxd SSH endpoint is disabled; restart sandboxd after enabling it"
create = AsyncMock(return_value=_process(returncode=1, stderr=disabled))
monkeypatch.setattr("providers.docker_sbx.api.asyncio.create_subprocess_exec", create)

with pytest.raises(DockerSbxTransportError, match="SSH endpoint is disabled"):
await SbxCLI().check_ssh_endpoint()


@pytest.mark.asyncio
async def test_check_ssh_endpoint_rejects_output_without_a_banner(monkeypatch):
create = AsyncMock(return_value=_process(stdout=b""))
monkeypatch.setattr("providers.docker_sbx.api.asyncio.create_subprocess_exec", create)

with pytest.raises(DockerSbxTransportError, match="no SSH banner"):
await SbxCLI().check_ssh_endpoint()


@pytest.mark.asyncio
async def test_remove_sandbox_forces_removal_of_an_attached_sandbox(monkeypatch):
captured: dict = {}
Expand Down
17 changes: 16 additions & 1 deletion src/providers/docker_sbx/tests/test_diagnose.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,23 @@ def _provider(api: MagicMock) -> DockerSbxProvider:
async def test_diagnose_reports_daemon_reachability():
api = MagicMock()
api.sandbox_count = AsyncMock(return_value=2)
api.check_ssh_endpoint = AsyncMock()

assert await _provider(api).diagnose() == "sandboxd reachable, 2 sandbox(es)"
assert (
await _provider(api).diagnose() == "sandboxd reachable, 2 sandbox(es), SSH endpoint ready"
)


@pytest.mark.asyncio
async def test_diagnose_raises_when_the_ssh_endpoint_is_unreachable():
api = MagicMock()
api.sandbox_count = AsyncMock(return_value=2)
api.check_ssh_endpoint = AsyncMock(
side_effect=DockerSbxTransportError("the sandboxd SSH endpoint is disabled")
)

with pytest.raises(DockerSbxTransportError):
await _provider(api).diagnose()


@pytest.mark.asyncio
Expand Down
Loading