Skip to content

Commit b39f5aa

Browse files
authored
feat(install-vm): install gateway + vm driver, add --driver-dir resolution (#887)
1 parent 8a813ab commit b39f5aa

6 files changed

Lines changed: 322 additions & 105 deletions

File tree

architecture/gateway.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ The gateway boots in `main()` (`crates/openshell-server/src/main.rs`) and procee
9999
1. Connect to the persistence store (`Store::connect`), which auto-detects SQLite vs Postgres from the URL prefix and runs migrations.
100100
2. Create `ComputeRuntime` with a `ComputeDriver` implementation selected by `OPENSHELL_DRIVERS`:
101101
- `kubernetes` wraps `KubernetesComputeDriver` in `ComputeDriverService`, so the gateway uses the `openshell.compute.v1.ComputeDriver` RPC surface even without transport.
102-
- `vm` spawns the sibling `openshell-driver-vm` binary as a local compute-driver process, connects to it over a Unix domain socket, and keeps the libkrun/rootfs runtime out of the gateway binary.
102+
- `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.
103103
3. Build `ServerState` (shared via `Arc<ServerState>` across all handlers).
104104
4. **Spawn background tasks**:
105105
- `ComputeRuntime::spawn_watchers` -- consumes the compute-driver watch stream, republishes platform events, and runs a periodic `ListSandboxes` snapshot reconcile so the store-backed public sandbox reads stay aligned with the compute driver.
@@ -128,7 +128,7 @@ All configuration is via CLI flags with environment variable fallbacks. The `--d
128128
| `--grpc-endpoint` | `OPENSHELL_GRPC_ENDPOINT` | None | gRPC endpoint reachable from within the cluster (for sandbox callbacks) |
129129
| `--drivers` | `OPENSHELL_DRIVERS` | `kubernetes` | Compute backend to use. Current options are `kubernetes` and `vm`. |
130130
| `--vm-driver-state-dir` | `OPENSHELL_VM_DRIVER_STATE_DIR` | `target/openshell-vm-driver` | Host directory for VM sandbox rootfs, console logs, and runtime state |
131-
| `--vm-compute-driver-bin` | `OPENSHELL_VM_COMPUTE_DRIVER_BIN` | sibling `openshell-driver-vm` binary | Local VM compute-driver process spawned by the gateway |
131+
| `--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. |
132132
| `--vm-krun-log-level` | `OPENSHELL_VM_KRUN_LOG_LEVEL` | `1` | libkrun log level for VM helper processes |
133133
| `--vm-driver-vcpus` | `OPENSHELL_VM_DRIVER_VCPUS` | `2` | Default vCPU count for VM sandboxes |
134134
| `--vm-driver-mem-mib` | `OPENSHELL_VM_DRIVER_MEM_MIB` | `2048` | Default memory allocation for VM sandboxes in MiB |

crates/openshell-driver-vm/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ target/debug/openshell-gateway \
9090
--vm-driver-state-dir $PWD/target/openshell-vm-driver-dev
9191
```
9292

93-
The gateway discovers `openshell-driver-vm` as a sibling of its own binary. Pass `--vm-compute-driver-bin /path/to/openshell-driver-vm` (or set `OPENSHELL_VM_COMPUTE_DRIVER_BIN`) to override.
93+
The gateway resolves `openshell-driver-vm` in this order: `--driver-dir`, conventional install locations (`~/.local/libexec/openshell`, `/usr/local/libexec/openshell`, `/usr/local/libexec`), then a sibling of the gateway binary.
9494

9595
## Flags
9696

@@ -99,7 +99,7 @@ The gateway discovers `openshell-driver-vm` as a sibling of its own binary. Pass
9999
| `--drivers vm` | `OPENSHELL_DRIVERS` | `kubernetes` | Select the VM compute driver. |
100100
| `--grpc-endpoint URL` | `OPENSHELL_GRPC_ENDPOINT` || Required. URL the sandbox guest calls back to. Use a host alias that resolves to the gateway's host from inside the VM (gvproxy answers `host.containers.internal` and `host.openshell.internal` to `192.168.127.1`). |
101101
| `--vm-driver-state-dir DIR` | `OPENSHELL_VM_DRIVER_STATE_DIR` | `target/openshell-vm-driver` | Per-sandbox rootfs, console logs, and the `compute-driver.sock` UDS. |
102-
| `--vm-compute-driver-bin PATH` | `OPENSHELL_VM_COMPUTE_DRIVER_BIN` | sibling of gateway binary | Override the driver binary path. |
102+
| `--driver-dir DIR` | `OPENSHELL_DRIVER_DIR` | unset | Override the directory searched for `openshell-driver-vm`. |
103103
| `--vm-driver-vcpus N` | `OPENSHELL_VM_DRIVER_VCPUS` | `2` | vCPUs per sandbox. |
104104
| `--vm-driver-mem-mib N` | `OPENSHELL_VM_DRIVER_MEM_MIB` | `2048` | Memory per sandbox, in MiB. |
105105
| `--vm-krun-log-level N` | `OPENSHELL_VM_KRUN_LOG_LEVEL` | `1` | libkrun verbosity (0–5). |

crates/openshell-driver-vm/start.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export OPENSHELL_SSH_GATEWAY_HOST="${OPENSHELL_SSH_GATEWAY_HOST:-127.0.0.1}"
5757
export OPENSHELL_SSH_GATEWAY_PORT="${OPENSHELL_SSH_GATEWAY_PORT:-${SERVER_PORT}}"
5858
export OPENSHELL_SSH_HANDSHAKE_SECRET="${OPENSHELL_SSH_HANDSHAKE_SECRET:-dev-vm-driver-secret}"
5959
export OPENSHELL_VM_DRIVER_STATE_DIR="${STATE_DIR}"
60-
export OPENSHELL_VM_COMPUTE_DRIVER_BIN="${OPENSHELL_VM_COMPUTE_DRIVER_BIN:-${ROOT}/target/debug/openshell-driver-vm}"
6160

6261
echo "==> Starting OpenShell server with VM compute driver"
6362
exec "${ROOT}/target/debug/openshell-gateway"

crates/openshell-server/src/cli.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,13 @@ struct Args {
121121
)]
122122
vm_driver_state_dir: PathBuf,
123123

124-
/// VM compute-driver binary spawned by the gateway.
125-
#[arg(long, env = "OPENSHELL_VM_COMPUTE_DRIVER_BIN")]
126-
vm_compute_driver_bin: Option<PathBuf>,
124+
/// Directory searched for compute-driver binaries (e.g.
125+
/// `openshell-driver-vm`) when an explicit binary override isn't
126+
/// configured. When unset, the gateway searches
127+
/// `$HOME/.local/libexec/openshell`, `/usr/local/libexec/openshell`,
128+
/// `/usr/local/libexec`, then a sibling of the gateway binary.
129+
#[arg(long, env = "OPENSHELL_DRIVER_DIR")]
130+
driver_dir: Option<PathBuf>,
127131

128132
/// libkrun log level used by the VM helper.
129133
#[arg(
@@ -262,7 +266,7 @@ async fn run_from_args(args: Args) -> Result<()> {
262266

263267
let vm_config = VmComputeConfig {
264268
state_dir: args.vm_driver_state_dir,
265-
compute_driver_bin: args.vm_compute_driver_bin,
269+
driver_dir: args.driver_dir,
266270
krun_log_level: args.vm_krun_log_level,
267271
vcpus: args.vm_vcpus,
268272
mem_mib: args.vm_mem_mib,

crates/openshell-server/src/compute/vm.rs

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,17 @@ use tonic::transport::Endpoint;
5151
#[cfg(unix)]
5252
use tower::service_fn;
5353

54+
const DRIVER_BIN_NAME: &str = "openshell-driver-vm";
55+
5456
/// Configuration for launching and talking to the VM compute driver.
5557
#[derive(Debug, Clone)]
5658
pub struct VmComputeConfig {
5759
/// Working directory for VM driver sandbox state.
5860
pub state_dir: PathBuf,
5961

60-
/// Optional override for the `openshell-driver-vm` binary path.
61-
/// When `None`, the gateway resolves a sibling of its own executable.
62-
pub compute_driver_bin: Option<PathBuf>,
62+
/// Directory to search for compute-driver binaries before the gateway
63+
/// falls back to its conventional install paths and sibling binary.
64+
pub driver_dir: Option<PathBuf>,
6365

6466
/// libkrun log level used by the VM driver helper.
6567
pub krun_log_level: u32,
@@ -104,13 +106,24 @@ impl VmComputeConfig {
104106
pub const fn default_mem_mib() -> u32 {
105107
2048
106108
}
109+
110+
#[must_use]
111+
fn default_driver_search_dirs(home: Option<PathBuf>) -> Vec<PathBuf> {
112+
let mut dirs = Vec::new();
113+
if let Some(home) = home {
114+
dirs.push(home.join(".local").join("libexec").join("openshell"));
115+
}
116+
push_unique_path(&mut dirs, PathBuf::from("/usr/local/libexec/openshell"));
117+
push_unique_path(&mut dirs, PathBuf::from("/usr/local/libexec"));
118+
dirs
119+
}
107120
}
108121

109122
impl Default for VmComputeConfig {
110123
fn default() -> Self {
111124
Self {
112125
state_dir: Self::default_state_dir(),
113-
compute_driver_bin: None,
126+
driver_dir: None,
114127
krun_log_level: Self::default_krun_log_level(),
115128
vcpus: Self::default_vcpus(),
116129
mem_mib: Self::default_mem_mib(),
@@ -129,31 +142,66 @@ pub(crate) struct VmGuestTlsPaths {
129142
pub(crate) key: PathBuf,
130143
}
131144

132-
/// Resolve the `openshell-driver-vm` binary path, falling back to a sibling
133-
/// of the gateway's own executable when an override is not supplied.
145+
/// Resolve the `openshell-driver-vm` binary path.
146+
///
147+
/// Resolution order:
148+
/// 1. `{driver_dir}/openshell-driver-vm`, where `driver_dir` comes from
149+
/// `--driver-dir` / `OPENSHELL_DRIVER_DIR`.
150+
/// 2. Conventional install directories:
151+
/// `~/.local/libexec/openshell`, `/usr/local/libexec/openshell`,
152+
/// `/usr/local/libexec`.
153+
/// 3. Sibling of the gateway's own executable (last-resort fallback so
154+
/// local development builds still work out of the box).
134155
pub(crate) fn resolve_compute_driver_bin(vm_config: &VmComputeConfig) -> Result<PathBuf> {
135-
let path = if let Some(path) = vm_config.compute_driver_bin.clone() {
136-
path
137-
} else {
138-
let current_exe = std::env::current_exe()
139-
.map_err(|e| Error::config(format!("failed to resolve current executable: {e}")))?;
140-
let Some(parent) = current_exe.parent() else {
141-
return Err(Error::config(format!(
142-
"current executable '{}' has no parent directory",
143-
current_exe.display()
144-
)));
145-
};
146-
parent.join("openshell-driver-vm")
147-
};
156+
let mut searched: Vec<PathBuf> = Vec::new();
157+
158+
// 1. Configured driver directory, or the conventional install locations
159+
// when no explicit override is configured.
160+
for dir in resolve_driver_search_dirs(vm_config) {
161+
let candidate = dir.join(DRIVER_BIN_NAME);
162+
if candidate.is_file() {
163+
return Ok(candidate);
164+
}
165+
push_unique_path(&mut searched, candidate);
166+
}
148167

149-
if !path.is_file() {
168+
// 2. Sibling-of-gateway fallback.
169+
let current_exe = std::env::current_exe()
170+
.map_err(|e| Error::config(format!("failed to resolve current executable: {e}")))?;
171+
let Some(parent) = current_exe.parent() else {
150172
return Err(Error::config(format!(
151-
"vm compute driver binary '{}' does not exist; set --vm-compute-driver-bin or OPENSHELL_VM_COMPUTE_DRIVER_BIN",
152-
path.display()
173+
"current executable '{}' has no parent directory",
174+
current_exe.display()
153175
)));
176+
};
177+
let sibling = parent.join(DRIVER_BIN_NAME);
178+
if sibling.is_file() {
179+
return Ok(sibling);
180+
}
181+
push_unique_path(&mut searched, sibling);
182+
183+
let searched_display = searched
184+
.iter()
185+
.map(|p| format!("'{}'", p.display()))
186+
.collect::<Vec<_>>()
187+
.join(", ");
188+
Err(Error::config(format!(
189+
"vm compute driver binary not found (searched {searched_display}); install it under --driver-dir / OPENSHELL_DRIVER_DIR, a conventional libexec path such as ~/.local/libexec/openshell or /usr/local/libexec{{,/openshell}}, or place it next to the gateway binary"
190+
)))
191+
}
192+
193+
fn resolve_driver_search_dirs(vm_config: &VmComputeConfig) -> Vec<PathBuf> {
194+
if let Some(dir) = vm_config.driver_dir.clone() {
195+
vec![dir]
196+
} else {
197+
VmComputeConfig::default_driver_search_dirs(std::env::var_os("HOME").map(PathBuf::from))
154198
}
199+
}
155200

156-
Ok(path)
201+
fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
202+
if !paths.iter().any(|existing| existing == &path) {
203+
paths.push(path);
204+
}
157205
}
158206

159207
/// Path of the Unix domain socket the driver will listen on.
@@ -353,10 +401,56 @@ async fn connect_compute_driver(socket_path: &std::path::Path) -> Result<Channel
353401

354402
#[cfg(all(test, unix))]
355403
mod tests {
356-
use super::{VmComputeConfig, compute_driver_guest_tls_paths};
404+
use super::{
405+
VmComputeConfig, compute_driver_guest_tls_paths, resolve_compute_driver_bin,
406+
resolve_driver_search_dirs,
407+
};
357408
use openshell_core::{Config, TlsConfig};
409+
use std::os::unix::fs::PermissionsExt;
410+
use std::path::PathBuf;
358411
use tempfile::tempdir;
359412

413+
#[test]
414+
fn resolve_driver_bin_uses_driver_dir_when_binary_present() {
415+
let dir = tempdir().unwrap();
416+
let bin = dir.path().join("openshell-driver-vm");
417+
std::fs::write(&bin, "#!/bin/sh\n").unwrap();
418+
std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)).unwrap();
419+
420+
let vm_config = VmComputeConfig {
421+
driver_dir: Some(dir.path().to_path_buf()),
422+
..Default::default()
423+
};
424+
assert_eq!(resolve_compute_driver_bin(&vm_config).unwrap(), bin);
425+
}
426+
427+
#[test]
428+
fn resolve_driver_bin_error_mentions_driver_dir_hint() {
429+
let dir = tempdir().unwrap(); // empty — no driver binary present
430+
431+
let vm_config = VmComputeConfig {
432+
driver_dir: Some(dir.path().to_path_buf()),
433+
..Default::default()
434+
};
435+
let err = resolve_compute_driver_bin(&vm_config)
436+
.unwrap_err()
437+
.to_string();
438+
assert!(err.contains("--driver-dir"));
439+
assert!(err.contains("OPENSHELL_DRIVER_DIR"));
440+
assert!(err.contains("openshell-driver-vm"));
441+
}
442+
443+
#[test]
444+
fn resolve_driver_search_dirs_include_usr_local_libexec_fallbacks() {
445+
let dirs = resolve_driver_search_dirs(&VmComputeConfig {
446+
driver_dir: None,
447+
..Default::default()
448+
});
449+
450+
assert!(dirs.contains(&PathBuf::from("/usr/local/libexec/openshell")));
451+
assert!(dirs.contains(&PathBuf::from("/usr/local/libexec")));
452+
}
453+
360454
#[test]
361455
fn vm_compute_driver_tls_requires_explicit_guest_bundle() {
362456
let dir = tempdir().unwrap();

0 commit comments

Comments
 (0)