Skip to content

Commit 2979bf9

Browse files
committed
feat(server): model remote compute driver endpoints
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 0925729 commit 2979bf9

9 files changed

Lines changed: 539 additions & 216 deletions

File tree

architecture/compute-runtimes.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ when a sandbox create request asks for GPU resources.
3333
| Docker | Local development with Docker available. | Container plus nested sandbox namespace. | Uses host networking so loopback gateway endpoints work from the supervisor. |
3434
| Podman | Rootless or single-machine deployments. | Container plus nested sandbox namespace. | Uses the Podman REST API, OCI image volumes, and CDI GPU devices when available. |
3535
| Kubernetes | Cluster deployment through Helm. | Pod plus nested sandbox namespace. | Uses Kubernetes API objects, service accounts, secrets, PVC-backed workspace storage, and GPU resources. |
36-
| VM | Experimental microVM isolation. | Per-sandbox libkrun VM. | Gateway spawns `openshell-driver-vm` as a subprocess over a private, state-local Unix socket. The VM driver boots a cached bootstrap `rootfs.ext4`, prepares requested OCI images inside a bootstrap VM with `umoci`, attaches the prepared image disk read-only, and gives each sandbox a writable `overlay.ext4` for merged-root changes and runtime material. The driver persists each accepted launch request beside the overlay and restarts those VMs on driver startup without recreating the overlay. |
37-
| External | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Activated by `--compute-driver-socket=<path>` (env `OPENSHELL_COMPUTE_DRIVER_SOCKET`). The gateway connects to a UDS the operator already provisioned, runs `GetCapabilities`, logs the advertised `driver_name`, and dispatches all sandbox lifecycle calls through the same `compute_driver.proto` surface as the in-tree drivers. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove the driver. The trust boundary is the socket's filesystem permissions the operator must ensure only the gateway uid can read/write it. |
36+
| VM | Experimental microVM isolation. | Per-sandbox libkrun VM. | Managed endpoint-backed driver. The gateway spawns `openshell-driver-vm`, waits for its Unix socket, and then consumes it through the same remote `compute_driver.proto` path used by unmanaged endpoint drivers. The VM driver boots a cached bootstrap `rootfs.ext4`, prepares requested OCI images inside a bootstrap VM with `umoci`, attaches the prepared image disk read-only, and gives each sandbox a writable `overlay.ext4` for merged-root changes and runtime material. The driver persists each accepted launch request beside the overlay and restarts those VMs on driver startup without recreating the overlay. |
37+
| Extension | Out-of-tree drivers operated alongside the gateway. | Whatever boundary the driver implements. | Selected by a non-reserved custom `compute_drivers = ["<name>"]` entry with `[openshell.drivers.<name>].socket_path`, or by `--compute-driver-socket=<path>` as launch-time shorthand for the `extension` driver ID. Reserved built-in names such as `vm`, `docker`, `podman`, and `kubernetes` cannot be used as unmanaged socket endpoints. The gateway connects to a UDS the operator already provisioned, runs `GetCapabilities`, logs the advertised `driver_name`, and dispatches all sandbox lifecycle calls through `compute_driver.proto`. The driver process and socket lifecycle are operator-owned; the gateway does not spawn, supervise, or remove unmanaged extension drivers. The trust boundary is the socket's filesystem permissions: the operator must ensure only the gateway uid can read/write it. |
3838

3939
Per-sandbox CPU and memory values currently enter the driver layer through
4040
template resource limits. Docker and Podman apply them as runtime limits.
@@ -79,7 +79,7 @@ The supervisor must be available inside each sandbox workload:
7979
| Podman | Read-only OCI image volume containing the supervisor binary. |
8080
| Kubernetes | Sandbox pod image or pod template configuration. |
8181
| VM | Embedded in the guest rootfs bundle. |
82-
| External | Defined by the out-of-tree driver. |
82+
| Extension | Defined by the out-of-tree driver. |
8383

8484
Driver-controlled environment variables must override sandbox image or template
8585
values for sandbox ID, sandbox name, gateway endpoint, relay socket path, TLS

crates/openshell-core/src/config.rs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! Configuration management for `OpenShell` components.
55
66
use serde::{Deserialize, Serialize};
7+
use std::collections::BTreeMap;
78
use std::fmt;
89
#[cfg(unix)]
910
use std::io::{Read, Write};
@@ -69,6 +70,27 @@ impl ComputeDriverKind {
6970
}
7071
}
7172

73+
/// Normalize a configured compute driver name.
74+
///
75+
/// Built-in driver names and custom remote driver names share the same
76+
/// selection namespace. The normalized value is lowercase ASCII and may contain
77+
/// letters, digits, `-`, and `_`.
78+
pub fn normalize_compute_driver_name(value: &str) -> Result<String, String> {
79+
let value = value.trim();
80+
if value.is_empty() {
81+
return Err("compute driver name cannot be empty".to_string());
82+
}
83+
if !value
84+
.bytes()
85+
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_'))
86+
{
87+
return Err(format!(
88+
"invalid compute driver name '{value}'. use ASCII letters, digits, '-' or '_'"
89+
));
90+
}
91+
Ok(value.to_ascii_lowercase())
92+
}
93+
7294
impl fmt::Display for ComputeDriverKind {
7395
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7496
f.write_str(self.as_str())
@@ -358,13 +380,14 @@ pub struct Config {
358380
/// The config shape allows multiple drivers so the gateway can evolve
359381
/// toward multi-backend routing. Current releases require exactly one
360382
/// configured driver.
361-
pub compute_drivers: Vec<ComputeDriverKind>,
383+
pub compute_drivers: Vec<String>,
362384

363-
/// When set, the gateway dispatches sandbox lifecycle to an out-of-tree
364-
/// compute driver process listening on this Unix domain socket and
365-
/// speaking `compute_driver.proto`. Takes precedence over
366-
/// `compute_drivers` and the auto-detection probe.
367-
pub external_compute_driver_socket: Option<PathBuf>,
385+
/// Operator-provided endpoints for named remote compute drivers.
386+
///
387+
/// This is populated by CLI/env inputs such as `--compute-driver-socket`.
388+
/// TOML-authored endpoints live under `[openshell.drivers.<name>]` and are
389+
/// resolved by the gateway config loader.
390+
pub compute_driver_endpoints: BTreeMap<String, PathBuf>,
368391

369392
/// TTL for SSH session tokens, in seconds. 0 disables expiry.
370393
pub ssh_session_ttl_secs: u64,
@@ -565,7 +588,7 @@ impl Config {
565588
gateway_jwt: None,
566589
database_url: String::new(),
567590
compute_drivers: vec![],
568-
external_compute_driver_socket: None,
591+
compute_driver_endpoints: BTreeMap::new(),
569592
ssh_session_ttl_secs: default_ssh_session_ttl_secs(),
570593
grpc_rate_limit_requests: None,
571594
grpc_rate_limit_window_secs: None,
@@ -621,18 +644,27 @@ impl Config {
621644

622645
/// Create a new configuration with the configured compute drivers.
623646
#[must_use]
624-
pub fn with_compute_drivers<I>(mut self, drivers: I) -> Self
647+
pub fn with_compute_drivers<I, D>(mut self, drivers: I) -> Self
625648
where
626-
I: IntoIterator<Item = ComputeDriverKind>,
649+
I: IntoIterator<Item = D>,
650+
D: ToString,
627651
{
628-
self.compute_drivers = drivers.into_iter().collect();
652+
self.compute_drivers = drivers
653+
.into_iter()
654+
.map(|driver| driver.to_string())
655+
.collect();
629656
self
630657
}
631658

632-
/// Pin an external compute driver by Unix domain socket path.
659+
/// Register a Unix domain socket endpoint for a named remote driver.
633660
#[must_use]
634-
pub fn with_external_compute_driver_socket(mut self, socket: Option<PathBuf>) -> Self {
635-
self.external_compute_driver_socket = socket;
661+
pub fn with_compute_driver_endpoint(
662+
mut self,
663+
name: impl Into<String>,
664+
socket: impl Into<PathBuf>,
665+
) -> Self {
666+
self.compute_driver_endpoints
667+
.insert(name.into(), socket.into());
636668
self
637669
}
638670

@@ -780,8 +812,8 @@ mod tests {
780812
use super::is_reachable_unix_socket;
781813
use super::{
782814
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayJwtConfig, detect_driver,
783-
docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
784-
podman_socket_responds,
815+
docker_host_unix_socket_path, is_unix_socket, normalize_compute_driver_name,
816+
podman_socket_candidates_from_env, podman_socket_responds,
785817
};
786818
#[cfg(unix)]
787819
use std::io::{Read as _, Write as _};
@@ -817,6 +849,18 @@ mod tests {
817849
assert!(err.contains("unsupported compute driver 'firecracker'"));
818850
}
819851

852+
#[test]
853+
fn compute_driver_name_normalization_accepts_builtin_and_custom_names() {
854+
assert_eq!(normalize_compute_driver_name(" VM ").unwrap(), "vm");
855+
assert_eq!(
856+
normalize_compute_driver_name("Kyma_GPU-1").unwrap(),
857+
"kyma_gpu-1"
858+
);
859+
860+
let err = normalize_compute_driver_name("kyma/gpu").unwrap_err();
861+
assert!(err.contains("invalid compute driver name"));
862+
}
863+
820864
#[test]
821865
fn config_defaults_to_loopback_bind_address() {
822866
let expected: SocketAddr = "127.0.0.1:17670".parse().expect("valid address");

crates/openshell-server/src/cli.rs

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@ struct RunArgs {
109109
value_delimiter = ',',
110110
value_parser = parse_compute_driver
111111
)]
112-
drivers: Vec<ComputeDriverKind>,
112+
drivers: Vec<String>,
113113

114-
/// Path to a Unix domain socket served by an out-of-tree compute driver
114+
/// Path to a Unix domain socket served by a remote compute driver
115115
/// implementing `compute_driver.proto`.
116116
///
117-
/// When set, the gateway dispatches sandbox lifecycle to that driver
118-
/// instead of one of the in-tree backends, skipping both the `--drivers`
119-
/// list and the auto-detection probe. The driver name advertised in
120-
/// `GetCapabilities` is logged for diagnostics.
117+
/// When set, the socket is associated with the single configured driver
118+
/// name. If no driver name is configured, the gateway uses `extension`.
119+
/// Reserved built-in driver names such as Docker, Podman, Kubernetes, and
120+
/// VM do not accept socket endpoints.
121121
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
122122
compute_driver_socket: Option<PathBuf>,
123123

@@ -245,6 +245,7 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> {
245245
if let Some(file) = file.as_ref() {
246246
merge_file_into_args(&mut args, &file.openshell.gateway, &matches);
247247
}
248+
normalize_compute_driver_socket_args(&mut args)?;
248249

249250
let local_tls = apply_runtime_defaults(&mut args)?;
250251
let local_jwt = defaults::complete_local_jwt_config()?;
@@ -375,13 +376,19 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> {
375376
args.grpc_rate_limit_requests,
376377
args.grpc_rate_limit_window_seconds,
377378
)
378-
.with_external_compute_driver_socket(args.compute_driver_socket.clone())
379379
.with_server_sans(args.server_sans.clone())
380380
.with_loopback_service_http(args.enable_loopback_service_http);
381381
validate_grpc_rate_limit_args(
382382
args.grpc_rate_limit_requests,
383383
args.grpc_rate_limit_window_seconds,
384384
)?;
385+
if let Some(socket) = args.compute_driver_socket.clone() {
386+
let driver = args
387+
.drivers
388+
.first()
389+
.expect("normalize_compute_driver_socket_args sets a driver for socket endpoints");
390+
config = config.with_compute_driver_endpoint(driver.clone(), socket);
391+
}
385392

386393
if let Some(ttl) = file
387394
.as_ref()
@@ -468,8 +475,8 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> {
468475
.into_diagnostic()
469476
}
470477

471-
fn parse_compute_driver(value: &str) -> std::result::Result<ComputeDriverKind, String> {
472-
value.parse()
478+
fn parse_compute_driver(value: &str) -> std::result::Result<String, String> {
479+
openshell_core::config::normalize_compute_driver_name(value)
473480
}
474481

475482
fn resolve_config_path(args: &RunArgs) -> Result<Option<PathBuf>> {
@@ -668,16 +675,51 @@ fn validate_grpc_rate_limit_args(requests: Option<u64>, window_seconds: Option<u
668675
Ok(())
669676
}
670677

671-
fn effective_single_driver(args: &RunArgs) -> Option<ComputeDriverKind> {
672-
// An external-driver socket pins dispatch to the out-of-tree path and
673-
// bypasses both the `--drivers` list and auto-detection probe; callers
674-
// that key off the in-tree `ComputeDriverKind` get `None` here.
675-
if args.compute_driver_socket.is_some() {
676-
return None;
678+
fn normalize_compute_driver_socket_args(args: &mut RunArgs) -> Result<()> {
679+
let Some(socket) = args.compute_driver_socket.as_ref() else {
680+
return Ok(());
681+
};
682+
if socket.as_os_str().is_empty() {
683+
return Err(miette::miette!(
684+
"--compute-driver-socket must not be an empty path"
685+
));
686+
}
687+
688+
match args.drivers.as_slice() {
689+
[] => {
690+
args.drivers.push("extension".to_string());
691+
Ok(())
692+
}
693+
[driver] => {
694+
let driver = openshell_core::config::normalize_compute_driver_name(driver)
695+
.map_err(|err| miette::miette!("{err}"))?;
696+
if matches!(
697+
driver.parse::<ComputeDriverKind>().ok(),
698+
Some(
699+
ComputeDriverKind::Docker
700+
| ComputeDriverKind::Podman
701+
| ComputeDriverKind::Kubernetes
702+
| ComputeDriverKind::Vm
703+
)
704+
) {
705+
return Err(miette::miette!(
706+
"--compute-driver-socket cannot be combined with reserved built-in compute driver '{driver}'"
707+
));
708+
}
709+
args.drivers[0] = driver;
710+
Ok(())
711+
}
712+
drivers => Err(miette::miette!(
713+
"--compute-driver-socket requires exactly one compute driver name, got: {}",
714+
drivers.join(",")
715+
)),
677716
}
717+
}
718+
719+
fn effective_single_driver(args: &RunArgs) -> Option<ComputeDriverKind> {
678720
match args.drivers.as_slice() {
679721
[] => openshell_core::config::detect_driver(),
680-
[driver] => Some(*driver),
722+
[driver] => driver.parse().ok(),
681723
_ => None,
682724
}
683725
}
@@ -1585,41 +1627,67 @@ ssh_session_ttl_secs = 1234
15851627
.unwrap_or_else(std::sync::PoisonError::into_inner);
15861628
let _g = EnvVarGuard::remove("OPENSHELL_COMPUTE_DRIVER_SOCKET");
15871629

1588-
let (args, _) = parse_with_args(&[
1630+
let (mut args, _) = parse_with_args(&[
15891631
"openshell-gateway",
15901632
"--db-url",
15911633
"sqlite::memory:",
15921634
"--compute-driver-socket",
1593-
"/run/openshell/external.sock",
1635+
"/run/openshell/extension.sock",
15941636
]);
1637+
super::normalize_compute_driver_socket_args(&mut args).unwrap();
15951638
assert_eq!(
15961639
args.compute_driver_socket.as_deref(),
1597-
Some(std::path::Path::new("/run/openshell/external.sock"))
1640+
Some(std::path::Path::new("/run/openshell/extension.sock"))
15981641
);
1599-
// External socket pins dispatch off the in-tree enum, so the
1600-
// single-driver helper must return None even when no --drivers given.
1642+
assert_eq!(args.drivers, ["extension"]);
16011643
assert!(super::effective_single_driver(&args).is_none());
16021644
}
16031645

16041646
#[test]
1605-
fn compute_driver_socket_overrides_drivers_flag() {
1647+
fn compute_driver_socket_rejects_reserved_builtin_drivers() {
16061648
let _lock = ENV_LOCK
16071649
.lock()
16081650
.unwrap_or_else(std::sync::PoisonError::into_inner);
16091651
let _g = EnvVarGuard::remove("OPENSHELL_COMPUTE_DRIVER_SOCKET");
16101652

1611-
let (args, _) = parse_with_args(&[
1653+
let (mut args, _) = parse_with_args(&[
16121654
"openshell-gateway",
16131655
"--db-url",
16141656
"sqlite::memory:",
16151657
"--drivers",
16161658
"docker",
16171659
"--compute-driver-socket",
1618-
"/run/openshell/external.sock",
1660+
"/run/openshell/extension.sock",
1661+
]);
1662+
let err = super::normalize_compute_driver_socket_args(&mut args).unwrap_err();
1663+
assert!(
1664+
err.to_string()
1665+
.contains("cannot be combined with reserved built-in compute driver 'docker'"),
1666+
"unexpected error: {err}"
1667+
);
1668+
}
1669+
1670+
#[test]
1671+
fn compute_driver_socket_rejects_vm_endpoint() {
1672+
let _lock = ENV_LOCK
1673+
.lock()
1674+
.unwrap_or_else(std::sync::PoisonError::into_inner);
1675+
let _g = EnvVarGuard::remove("OPENSHELL_COMPUTE_DRIVER_SOCKET");
1676+
1677+
let (mut args, _) = parse_with_args(&[
1678+
"openshell-gateway",
1679+
"--db-url",
1680+
"sqlite::memory:",
1681+
"--drivers",
1682+
"vm",
1683+
"--compute-driver-socket",
1684+
"/run/openshell/vm.sock",
16191685
]);
1686+
let err = super::normalize_compute_driver_socket_args(&mut args).unwrap_err();
16201687
assert!(
1621-
super::effective_single_driver(&args).is_none(),
1622-
"external socket must short-circuit --drivers"
1688+
err.to_string()
1689+
.contains("cannot be combined with reserved built-in compute driver 'vm'"),
1690+
"unexpected error: {err}"
16231691
);
16241692
}
16251693

@@ -1630,14 +1698,16 @@ ssh_session_ttl_secs = 1234
16301698
.unwrap_or_else(std::sync::PoisonError::into_inner);
16311699
let _g = EnvVarGuard::set(
16321700
"OPENSHELL_COMPUTE_DRIVER_SOCKET",
1633-
"/var/run/openshell/external.sock",
1701+
"/var/run/openshell/extension.sock",
16341702
);
16351703

1636-
let (args, _) = parse_with_args(&["openshell-gateway", "--db-url", "sqlite::memory:"]);
1704+
let (mut args, _) = parse_with_args(&["openshell-gateway", "--db-url", "sqlite::memory:"]);
1705+
super::normalize_compute_driver_socket_args(&mut args).unwrap();
16371706
assert_eq!(
16381707
args.compute_driver_socket.as_deref(),
1639-
Some(std::path::Path::new("/var/run/openshell/external.sock"))
1708+
Some(std::path::Path::new("/var/run/openshell/extension.sock"))
16401709
);
1710+
assert_eq!(args.drivers, ["extension"]);
16411711
}
16421712

16431713
#[test]

0 commit comments

Comments
 (0)