Skip to content

Commit 5975805

Browse files
authored
feat(server): add bundled docker compute driver (#888)
1 parent 385855c commit 5975805

19 files changed

Lines changed: 3707 additions & 32 deletions

File tree

‎.agents/skills/debug-openshell-cluster/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ Component images (server, sandbox) can reach kubelet via two paths:
184184

185185
**Local/external pull mode** (default local via `mise run cluster`): Local images are tagged to the configured local registry base (default `127.0.0.1:5000/openshell/*`), pushed to that registry, and pulled by k3s via `registries.yaml` mirror endpoint (typically `host.docker.internal:5000`). The `cluster` task pushes prebuilt local tags (`openshell/*:dev`, falling back to `localhost:5000/openshell/*:dev` or `127.0.0.1:5000/openshell/*:dev`).
186186

187-
Gateway image builds now stage a partial Rust workspace from `deploy/docker/Dockerfile.images`. If cargo fails with a missing manifest under `/build/crates/...`, or an imported symbol exists locally but is missing in the image build, verify that every current gateway dependency crate (including `openshell-driver-kubernetes` and `openshell-ocsf`) is copied into the staged workspace there.
187+
Gateway image builds now stage a partial Rust workspace from `deploy/docker/Dockerfile.images`. If cargo fails with a missing manifest under `/build/crates/...`, or an imported symbol exists locally but is missing in the image build, verify that every current gateway dependency crate (including `openshell-driver-docker`, `openshell-driver-kubernetes`, and `openshell-ocsf`) is copied into the staged workspace there.
188188

189189
```bash
190190
# Verify image refs currently used by openshell deployment

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
4141
| `crates/openshell-tui/` | Terminal UI | Ratatui-based dashboard for monitoring |
4242
| `crates/openshell-vm/` | MicroVM runtime | Experimental, work-in-progress libkrun-based VM execution |
4343
| `crates/openshell-driver-kubernetes/` | Kubernetes compute driver | In-process `ComputeDriver` backend for K8s sandbox pods |
44+
| `crates/openshell-driver-docker/` | Docker compute driver | In-process `ComputeDriver` backend for local Docker sandbox containers |
4445
| `crates/openshell-driver-vm/` | VM compute driver | Standalone libkrun-backed `ComputeDriver` subprocess (embeds its own rootfs + runtime) |
4546
| `python/openshell/` | Python SDK | Python bindings and CLI packaging |
4647
| `proto/` | Protobuf definitions | gRPC service contracts |

‎Cargo.lock‎

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎architecture/gateway.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ graph TD
7676
| Persistence | `crates/openshell-server/src/persistence/mod.rs` | `Store` enum (SQLite/Postgres), generic object CRUD, protobuf codec |
7777
| Compute runtime | `crates/openshell-server/src/compute/mod.rs` | `ComputeRuntime`, gateway-owned sandbox lifecycle orchestration over a compute backend |
7878
| Compute driver: Kubernetes | `crates/openshell-driver-kubernetes/src/driver.rs` | Kubernetes CRD create/delete/watch, pod template translation |
79+
| Compute driver: Docker | `crates/openshell-driver-docker/src/lib.rs` | Local Docker container create/stop/delete/watch |
7980
| Compute driver: VM | `crates/openshell-driver-vm/src/driver.rs` | Per-sandbox microVM create/delete/watch, supervisor-only guest boot |
8081
| Sandbox index | `crates/openshell-server/src/sandbox_index.rs` | `SandboxIndex` -- in-memory name/pod-to-id correlation |
8182
| Watch bus | `crates/openshell-server/src/sandbox_watch.rs` | `SandboxWatchBus` -- in-memory broadcast for persisted sandbox updates |
@@ -103,6 +104,7 @@ The gateway boots in `cli::run_cli` (`crates/openshell-server/src/cli.rs`) and p
103104
1. Connect to the persistence store (`Store::connect`), which auto-detects SQLite vs Postgres from the URL prefix and runs migrations.
104105
2. Create `ComputeRuntime` with a `ComputeDriver` implementation selected by `OPENSHELL_DRIVERS`:
105106
- `kubernetes` wraps `KubernetesComputeDriver` in `ComputeDriverService`, so the gateway uses the `openshell.compute.v1.ComputeDriver` RPC surface even without transport.
107+
- `docker` constructs `openshell-driver-docker` in-process and manages local containers labeled with the configured sandbox namespace.
106108
- `vm` spawns the standalone `openshell-driver-vm` binary as a local compute-driver process, resolves it from `--driver-dir`, conventional libexec install paths, or a sibling of the gateway binary, connects to it over a Unix domain socket, and keeps the libkrun/rootfs runtime out of the gateway binary.
107109
3. Build `ServerState` (shared via `Arc<ServerState>` across all handlers), including a fresh `SupervisorSessionRegistry`.
108110
4. **Spawn background tasks**:
@@ -116,7 +118,7 @@ The gateway boots in `cli::run_cli` (`crates/openshell-server/src/cli.rs`) and p
116118

117119
## Configuration
118120

119-
All configuration is via CLI flags with environment variable fallbacks. The `--db-url` and `--ssh-handshake-secret` flags are required.
121+
All configuration is via CLI flags with environment variable fallbacks. The `--db-url` flag is required. The `--ssh-handshake-secret` flag is required for non-Docker drivers; Docker sandboxes do not receive a handshake secret.
120122

121123
| Flag | Env Var | Default | Description |
122124
|------|---------|---------|-------------|
@@ -132,7 +134,7 @@ All configuration is via CLI flags with environment variable fallbacks. The `--d
132134
| `--sandbox-namespace` | `OPENSHELL_SANDBOX_NAMESPACE` | `default` | Kubernetes namespace for sandbox CRDs |
133135
| `--sandbox-image` | `OPENSHELL_SANDBOX_IMAGE` | None | Default container image for sandbox pods |
134136
| `--grpc-endpoint` | `OPENSHELL_GRPC_ENDPOINT` | None | gRPC endpoint reachable from within the cluster (for supervisor callbacks) |
135-
| `--drivers` | `OPENSHELL_DRIVERS` | `kubernetes` | Compute backend to use. Current options are `kubernetes` and `vm`. |
137+
| `--drivers` | `OPENSHELL_DRIVERS` | `kubernetes` | Compute backend to use. Current options are `kubernetes`, `docker`, and `vm`. |
136138
| `--vm-driver-state-dir` | `OPENSHELL_VM_DRIVER_STATE_DIR` | `target/openshell-vm-driver` | Host directory for VM sandbox rootfs, console logs, and runtime state |
137139
| `--driver-dir` | `OPENSHELL_DRIVER_DIR` | unset | Override directory for `openshell-driver-vm`. When unset, the gateway searches `~/.local/libexec/openshell`, `/usr/local/libexec/openshell`, `/usr/local/libexec`, then a sibling binary. |
138140
| `--vm-krun-log-level` | `OPENSHELL_VM_KRUN_LOG_LEVEL` | `1` | libkrun log level for VM helper processes |
@@ -600,6 +602,18 @@ The Helm chart template is at `deploy/helm/openshell/templates/statefulset.yaml`
600602

601603
The gateway reaches the sandbox exclusively through the supervisor-initiated `ConnectSupervisor` session, so the driver never returns sandbox network endpoints.
602604

605+
### Docker Driver
606+
607+
The Docker driver (`crates/openshell-driver-docker/src/lib.rs`) is an in-process compute backend for local standalone gateways. It creates one Docker container per sandbox, labels each container with `openshell.ai/managed-by=openshell`, `openshell.ai/sandbox-id`, `openshell.ai/sandbox-name`, and `openshell.ai/sandbox-namespace`, and bind-mounts a Linux `openshell-sandbox` supervisor binary into the container.
608+
609+
- **Create**: Pulls or validates the sandbox image according to `sandbox_image_pull_policy`, creates a labeled container, mounts the supervisor binary and optional TLS material, and starts the container with the supervisor as entrypoint.
610+
- **List/Get/Watch**: Reads labeled containers in the configured sandbox namespace and derives driver-native sandbox status from Docker state plus supervisor relay readiness.
611+
- **Stop**: Stops the matching labeled container without deleting it.
612+
- **Delete**: Force-removes the matching labeled container.
613+
- **Gateway shutdown**: On SIGINT or SIGTERM, `run_server()` leaves the accept loop and calls the Docker shutdown cleanup hook. The hook stops all running, restarting, or paused OpenShell-managed containers in the configured sandbox namespace so local sandboxes do not keep running after the gateway exits.
614+
- **Gateway startup resume**: Before the watch and reconcile loops spawn, `ComputeRuntime::resume_persisted_sandboxes()` walks every sandbox record in the store. For each sandbox whose phase is `Provisioning`, `Ready`, or `Unknown`, it asks the Docker driver to start the labeled container if it is in the `exited` or `created` state (`StartupResume::resume_sandbox`). Containers in `running` or `restarting` are left alone; `paused`, `dead`, and `removing` are skipped. If the matching container has disappeared, the sandbox is moved to phase `Error` with reason `BackendResourceMissing`; if the start call fails, the sandbox moves to `Error` with reason `ResumeFailed`. This is what makes sandboxes survive a graceful gateway restart end-to-end: shutdown stops them, the next startup resumes them, and the store remains the source of truth across the cycle. Drivers that do not need this hook (Kubernetes, Podman, VM) leave `startup_resume = None`, which makes the resume sweep a no-op.
615+
- **Handshake secret**: The Docker driver does not inject `OPENSHELL_SSH_HANDSHAKE_SECRET` or `OPENSHELL_SSH_HANDSHAKE_SKEW_SECS` into containers. Supervisor relay auth relies on the gateway connection rather than a Docker-visible container env secret.
616+
603617
### VM Driver
604618

605619
`VmDriver` (`crates/openshell-driver-vm/src/driver.rs`) is served by the standalone `openshell-driver-vm` process. The gateway spawns that binary on demand and talks to it over the internal `openshell.compute.v1.ComputeDriver` gRPC contract via a Unix domain socket.

‎architecture/sandbox-connect.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ The sandbox SSH daemon's exit thread waits for the reader thread to finish forwa
623623

624624
### Sandbox environment variables
625625

626-
These are injected into compute-backed sandboxes by the **Kubernetes** driver (`crates/openshell-driver-kubernetes/src/driver.rs`) and the **Podman** driver (`crates/openshell-driver-podman/src/container.rs`). Together they are required for **persistent `ConnectSupervisor` registration and relay** (see [Podman and relay environment](#podman-and-relay-environment) for the Podman-specific fix):
626+
These are injected into compute-backed sandboxes by the **Kubernetes** driver (`crates/openshell-driver-kubernetes/src/driver.rs`), the **Podman** driver (`crates/openshell-driver-podman/src/container.rs`), and the **Docker** driver (`crates/openshell-driver-docker/src/lib.rs`). Together they are required for **persistent `ConnectSupervisor` registration and relay** (see [Podman and relay environment](#podman-and-relay-environment) for the Podman-specific fix):
627627

628628
| Variable | Description |
629629
|---|---|

‎crates/openshell-core/src/config.rs‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub const CDI_GPU_DEVICE_ALL: &str = "nvidia.com/gpu=all";
4848
pub enum ComputeDriverKind {
4949
Kubernetes,
5050
Vm,
51+
Docker,
5152
Podman,
5253
}
5354

@@ -57,6 +58,7 @@ impl ComputeDriverKind {
5758
match self {
5859
Self::Kubernetes => "kubernetes",
5960
Self::Vm => "vm",
61+
Self::Docker => "docker",
6062
Self::Podman => "podman",
6163
}
6264
}
@@ -75,9 +77,10 @@ impl FromStr for ComputeDriverKind {
7577
match value.trim().to_ascii_lowercase().as_str() {
7678
"kubernetes" => Ok(Self::Kubernetes),
7779
"vm" => Ok(Self::Vm),
80+
"docker" => Ok(Self::Docker),
7881
"podman" => Ok(Self::Podman),
7982
other => Err(format!(
80-
"unsupported compute driver '{other}'. expected one of: kubernetes, vm, podman"
83+
"unsupported compute driver '{other}'. expected one of: kubernetes, vm, docker, podman"
8184
)),
8285
}
8386
}
@@ -450,12 +453,16 @@ mod tests {
450453
"podman".parse::<ComputeDriverKind>().unwrap(),
451454
ComputeDriverKind::Podman
452455
);
456+
assert_eq!(
457+
"docker".parse::<ComputeDriverKind>().unwrap(),
458+
ComputeDriverKind::Docker
459+
);
453460
}
454461

455462
#[test]
456463
fn compute_driver_kind_rejects_unknown_values() {
457-
let err = "docker".parse::<ComputeDriverKind>().unwrap_err();
458-
assert!(err.contains("unsupported compute driver 'docker'"));
464+
let err = "firecracker".parse::<ComputeDriverKind>().unwrap_err();
465+
assert!(err.contains("unsupported compute driver 'firecracker'"));
459466
}
460467

461468
#[test]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "openshell-driver-docker"
6+
description = "Docker compute driver for OpenShell"
7+
version.workspace = true
8+
edition.workspace = true
9+
rust-version.workspace = true
10+
license.workspace = true
11+
repository.workspace = true
12+
13+
[dependencies]
14+
openshell-core = { path = "../openshell-core" }
15+
16+
tokio = { workspace = true }
17+
tonic = { workspace = true }
18+
futures = { workspace = true }
19+
tokio-stream = { workspace = true }
20+
tracing = { workspace = true }
21+
bytes = { workspace = true }
22+
url = { workspace = true }
23+
bollard = { version = "0.20" }
24+
tar = "0.4"
25+
tempfile = "3"
26+
27+
[lints]
28+
workspace = true

0 commit comments

Comments
 (0)