Skip to content

Commit 5fbdf21

Browse files
committed
feat: add --compute-driver-socket flag and External driver variant
Add ComputeDriverKind::External that connects to a pre-existing Unix domain socket instead of spawning a driver subprocess. This enables out-of-process compute drivers running as sidecar containers. Changes: - Add External variant to ComputeDriverKind enum with parser/display - Add --compute-driver-socket CLI flag (+ OPENSHELL_COMPUTE_DRIVER_SOCKET env) - Add --credentials-driver-socket CLI flag (+ OPENSHELL_CREDENTIALS_DRIVER_SOCKET env) - Add compute_driver_socket and credentials_driver_socket to Config - Add compute::external module with UDS connection logic (retry loop) - Wire External arm in build_compute_runtime() reusing RemoteComputeDriver - Add tests for External variant parsing and config acceptance Ref: rossoctl/rossoctl#1353 Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
1 parent 567abde commit 5fbdf21

5 files changed

Lines changed: 159 additions & 2 deletions

File tree

crates/openshell-core/src/config.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub enum ComputeDriverKind {
4949
Kubernetes,
5050
Vm,
5151
Podman,
52+
External,
5253
}
5354

5455
impl ComputeDriverKind {
@@ -58,6 +59,7 @@ impl ComputeDriverKind {
5859
Self::Kubernetes => "kubernetes",
5960
Self::Vm => "vm",
6061
Self::Podman => "podman",
62+
Self::External => "external",
6163
}
6264
}
6365
}
@@ -76,8 +78,9 @@ impl FromStr for ComputeDriverKind {
7678
"kubernetes" => Ok(Self::Kubernetes),
7779
"vm" => Ok(Self::Vm),
7880
"podman" => Ok(Self::Podman),
81+
"external" => Ok(Self::External),
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, podman, external"
8184
)),
8285
}
8386
}
@@ -196,6 +199,18 @@ pub struct Config {
196199
/// allowing them to reach services running on the Docker host.
197200
#[serde(default)]
198201
pub host_gateway_ip: String,
202+
203+
/// Unix domain socket path to an external compute driver.
204+
/// When set with `--driver external`, the gateway connects to this
205+
/// pre-existing socket instead of spawning a driver subprocess.
206+
#[serde(default)]
207+
pub compute_driver_socket: String,
208+
209+
/// Unix domain socket path to a credentials driver.
210+
/// When set, the gateway delegates credential resolution to this
211+
/// out-of-process driver via the `CredentialsDriver` gRPC contract.
212+
#[serde(default)]
213+
pub credentials_driver_socket: String,
199214
}
200215

201216
/// TLS configuration.
@@ -308,6 +323,8 @@ impl Config {
308323
ssh_session_ttl_secs: default_ssh_session_ttl_secs(),
309324
client_tls_secret_name: String::new(),
310325
host_gateway_ip: String::new(),
326+
compute_driver_socket: String::new(),
327+
credentials_driver_socket: String::new(),
311328
}
312329
}
313330

@@ -451,6 +468,20 @@ impl Config {
451468
self.oidc = Some(oidc);
452469
self
453470
}
471+
472+
/// Set the Unix domain socket path for an external compute driver.
473+
#[must_use]
474+
pub fn with_compute_driver_socket(mut self, path: impl Into<String>) -> Self {
475+
self.compute_driver_socket = path.into();
476+
self
477+
}
478+
479+
/// Set the Unix domain socket path for a credentials driver.
480+
#[must_use]
481+
pub fn with_credentials_driver_socket(mut self, path: impl Into<String>) -> Self {
482+
self.credentials_driver_socket = path.into();
483+
self
484+
}
454485
}
455486

456487
fn default_bind_address() -> SocketAddr {
@@ -522,6 +553,14 @@ mod tests {
522553
);
523554
}
524555

556+
#[test]
557+
fn compute_driver_kind_parses_external() {
558+
assert_eq!(
559+
"external".parse::<ComputeDriverKind>().unwrap(),
560+
ComputeDriverKind::External
561+
);
562+
}
563+
525564
#[test]
526565
fn compute_driver_kind_rejects_unknown_values() {
527566
let err = "docker".parse::<ComputeDriverKind>().unwrap_err();

crates/openshell-server/src/cli.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ struct Args {
233233
/// Keycloak: "scope". Okta: "scp". Leave empty to disable scope enforcement.
234234
#[arg(long, env = "OPENSHELL_OIDC_SCOPES_CLAIM", default_value = "")]
235235
oidc_scopes_claim: String,
236+
237+
/// Unix domain socket path to an external compute driver.
238+
/// Use with `--driver external` to delegate ComputeDriver RPCs to a
239+
/// pre-existing out-of-process driver (e.g. a sidecar container).
240+
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
241+
compute_driver_socket: Option<PathBuf>,
242+
243+
/// Unix domain socket path to a credentials driver.
244+
/// When set, the gateway delegates credential resolution to this
245+
/// out-of-process driver via the CredentialsDriver gRPC contract.
246+
#[arg(long, env = "OPENSHELL_CREDENTIALS_DRIVER_SOCKET")]
247+
credentials_driver_socket: Option<PathBuf>,
236248
}
237249

238250
pub fn command() -> Command {
@@ -349,6 +361,14 @@ async fn run_from_args(args: Args) -> Result<()> {
349361
config = config.with_host_gateway_ip(ip);
350362
}
351363

364+
if let Some(socket) = args.compute_driver_socket {
365+
config = config.with_compute_driver_socket(socket.to_string_lossy());
366+
}
367+
368+
if let Some(socket) = args.credentials_driver_socket {
369+
config = config.with_credentials_driver_socket(socket.to_string_lossy());
370+
}
371+
352372
if let Some(issuer) = args.oidc_issuer {
353373
config = config.with_oidc(openshell_core::OidcConfig {
354374
issuer,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 Kagenti Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! External compute driver: connect to a pre-existing Unix domain socket.
5+
//!
6+
//! Unlike the VM driver ([`super::vm`]) which spawns and manages a subprocess,
7+
//! the external driver assumes the socket is already listening (e.g. a sidecar
8+
//! container in the same pod). The gateway just connects to it.
9+
10+
#[cfg(unix)]
11+
use hyper_util::rt::TokioIo;
12+
use openshell_core::{Error, Result};
13+
#[cfg(unix)]
14+
use std::time::Duration;
15+
#[cfg(unix)]
16+
use tokio::net::UnixStream;
17+
use tonic::transport::Channel;
18+
#[cfg(unix)]
19+
use tonic::transport::Endpoint;
20+
#[cfg(unix)]
21+
use tower::service_fn;
22+
23+
/// Connect to an external compute driver at the given Unix domain socket path.
24+
///
25+
/// Retries for up to 10 seconds to allow the sidecar time to start.
26+
#[cfg(unix)]
27+
pub(crate) async fn connect(socket_path: &std::path::Path) -> Result<Channel> {
28+
let mut last_error: Option<String> = None;
29+
for _ in 0..100 {
30+
match connect_once(socket_path).await {
31+
Ok(channel) => return Ok(channel),
32+
Err(err) => last_error = Some(err.to_string()),
33+
}
34+
tokio::time::sleep(Duration::from_millis(100)).await;
35+
}
36+
37+
Err(Error::execution(format!(
38+
"timed out waiting for external compute driver socket '{}': {}",
39+
socket_path.display(),
40+
last_error.unwrap_or_else(|| "unknown error".to_string())
41+
)))
42+
}
43+
44+
#[cfg(unix)]
45+
async fn connect_once(socket_path: &std::path::Path) -> Result<Channel> {
46+
let socket_path = socket_path.to_path_buf();
47+
let display_path = socket_path.clone();
48+
Endpoint::from_static("http://[::]:50051")
49+
.connect_with_connector(service_fn(move |_: tonic::transport::Uri| {
50+
let socket_path = socket_path.clone();
51+
async move { UnixStream::connect(socket_path).await.map(TokioIo::new) }
52+
}))
53+
.await
54+
.map_err(|e| {
55+
Error::execution(format!(
56+
"failed to connect to external compute driver socket '{}': {e}",
57+
display_path.display()
58+
))
59+
})
60+
}
61+
62+
#[cfg(not(unix))]
63+
pub(crate) async fn connect(_socket_path: &std::path::Path) -> Result<Channel> {
64+
Err(Error::config(
65+
"the external compute driver requires unix domain socket support",
66+
))
67+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
//! Gateway-owned compute orchestration over a pluggable compute backend.
55
6+
pub(crate) mod external;
67
pub mod vm;
78

89
pub use vm::VmComputeConfig;

crates/openshell-server/src/lib.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,26 @@ async fn build_compute_runtime(
408408
.await
409409
.map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))
410410
}
411+
ComputeDriverKind::External => {
412+
if config.compute_driver_socket.is_empty() {
413+
return Err(Error::config(
414+
"--compute-driver-socket is required when using the external compute driver",
415+
));
416+
}
417+
let socket_path = std::path::Path::new(&config.compute_driver_socket);
418+
let channel = compute::external::connect(socket_path).await?;
419+
ComputeRuntime::new_remote_vm(
420+
channel,
421+
None,
422+
store,
423+
sandbox_index,
424+
sandbox_watch_bus,
425+
tracing_log_bus,
426+
supervisor_sessions,
427+
)
428+
.await
429+
.map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))
430+
}
411431
}
412432
}
413433

@@ -419,7 +439,8 @@ fn configured_compute_driver(config: &Config) -> Result<ComputeDriverKind> {
419439
[
420440
driver @ (ComputeDriverKind::Kubernetes
421441
| ComputeDriverKind::Vm
422-
| ComputeDriverKind::Podman),
442+
| ComputeDriverKind::Podman
443+
| ComputeDriverKind::External),
423444
] => Ok(*driver),
424445
drivers => Err(Error::config(format!(
425446
"multiple compute drivers are not supported yet; configured drivers: {}",
@@ -494,4 +515,13 @@ mod tests {
494515
ComputeDriverKind::Vm
495516
);
496517
}
518+
519+
#[test]
520+
fn configured_compute_driver_accepts_external() {
521+
let config = Config::new(None).with_compute_drivers([ComputeDriverKind::External]);
522+
assert_eq!(
523+
configured_compute_driver(&config).unwrap(),
524+
ComputeDriverKind::External
525+
);
526+
}
497527
}

0 commit comments

Comments
 (0)