From e06866262ea17f8801f2426b72b6464341404a8f Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Wed, 10 Jun 2026 14:25:57 -0400 Subject: [PATCH 1/5] fix(core): fall back to podman CLI when no API socket is found (#1834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Podman API socket symlink is not always present — it varies by version, machine provider, and platform. When none of the candidate socket paths respond, try `podman info` as a fallback so auto-detection succeeds on macOS setups where Podman is functional but the socket is not at a well-known path. Closes #1834 Signed-off-by: Russell Bryant --- crates/openshell-core/src/config.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index 04d6928da..a8fa8a076 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -11,7 +11,7 @@ use std::net::SocketAddr; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use std::str::FromStr; #[cfg(unix)] use std::time::Duration; @@ -130,6 +130,20 @@ fn is_podman_available() -> bool { podman_socket_candidates() .iter() .any(|path| podman_socket_responds(path)) + || podman_cli_responds() +} + +/// The Podman API socket symlink is not always present (it varies by +/// version, machine provider, and platform). Fall back to the CLI, +/// which has its own internal discovery for reaching the machine. +fn podman_cli_responds() -> bool { + Command::new("podman") + .arg("info") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output() + .is_ok_and(|o| o.status.success()) } fn podman_socket_candidates() -> Vec { From f0022f8dfa6db2699080812bcb8efcaf5cce3979 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 12 Jun 2026 12:10:43 -0400 Subject: [PATCH 2/5] fix(core): discover Podman API socket path during auto-detection Auto-detection now queries `podman info --format json` to find the actual API socket when no well-known socket path responds. On macOS (serviceIsRemote=true) it follows up with `podman machine inspect` to get the host-side forwarded socket; on native Linux it uses remoteSocket.path directly. The discovered path is threaded into PodmanComputeConfig so the driver connects to the right socket instead of falling back to a default that may not exist. Closes #1834 Signed-off-by: Russell Bryant --- crates/openshell-core/src/config.rs | 232 +++++++++++++++++++++++++--- crates/openshell-core/src/lib.rs | 4 +- crates/openshell-server/src/cli.rs | 2 +- crates/openshell-server/src/lib.rs | 53 ++++--- 4 files changed, 244 insertions(+), 47 deletions(-) diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index a8fa8a076..b0ce32033 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -92,6 +92,17 @@ impl FromStr for ComputeDriverKind { } } +/// Result of [`detect_driver`]: the driver kind plus any connection +/// metadata discovered during probing (e.g. the Podman API socket path). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DetectedDriver { + pub kind: ComputeDriverKind, + /// Socket path discovered during detection (Podman only, for now). + /// `None` when the socket was found via the static candidate list + /// (which the driver already knows about) or for non-Podman drivers. + pub socket_path: Option, +} + /// Auto-detect the appropriate compute driver based on the runtime environment. /// /// Priority order: Kubernetes → Podman → Docker. @@ -99,20 +110,30 @@ impl FromStr for ComputeDriverKind { /// /// Returns the first driver where the environment check passes. /// Returns `None` if no compatible driver is found. -pub fn detect_driver() -> Option { +pub fn detect_driver() -> Option { // Kubernetes: check for KUBERNETES_SERVICE_HOST env var (set inside pods) if std::env::var_os("KUBERNETES_SERVICE_HOST").is_some() { - return Some(ComputeDriverKind::Kubernetes); + return Some(DetectedDriver { + kind: ComputeDriverKind::Kubernetes, + socket_path: None, + }); } - // Podman: check for a reachable local API socket. - if is_podman_available() { - return Some(ComputeDriverKind::Podman); + // Podman: check for a reachable local API socket, falling back to CLI + // discovery which also resolves the host-side socket path. + if let Some(socket_path) = detect_podman() { + return Some(DetectedDriver { + kind: ComputeDriverKind::Podman, + socket_path, + }); } // Docker: check if the CLI is available or a local Docker socket exists. if is_docker_available() { - return Some(ComputeDriverKind::Docker); + return Some(DetectedDriver { + kind: ComputeDriverKind::Docker, + socket_path: None, + }); } None @@ -126,24 +147,96 @@ fn is_binary_available(name: &str) -> bool { .is_ok_and(|output| output.status.success()) } -fn is_podman_available() -> bool { - podman_socket_candidates() +/// Detect whether Podman is available and, when the socket is not at a +/// well-known path, discover the host-side API socket via the CLI. +/// +/// Returns `Some(socket_path)` where `socket_path` is: +/// - `Some(path)` when the socket was discovered dynamically (CLI fallback), +/// - `None` when a static candidate already responded (the driver knows +/// those paths and will find it again). +/// +/// Returns `None` (outer) when Podman is not available at all. +fn detect_podman() -> Option> { + // Fast path: one of the well-known socket candidates responds. + if podman_socket_candidates() .iter() .any(|path| podman_socket_responds(path)) - || podman_cli_responds() + { + return Some(None); + } + + // Slow path: the socket symlink is missing or at a non-standard + // location. Ask the CLI to discover the host-side socket. + discover_podman_socket() +} + +/// Query the Podman CLI to discover the host-side API socket path. +/// +/// Strategy: +/// 1. Run `podman info --format json` to check connectivity and whether +/// the service is remote (macOS/Windows VM) or local (native Linux). +/// 2. If `serviceIsRemote` is true, run `podman machine inspect` to get +/// the host-side forwarded socket (the `remoteSocket` from `podman info` +/// is the VM-internal path, which is not reachable from the host). +/// 3. If `serviceIsRemote` is false, use `remoteSocket.path` directly +/// (on native Linux this IS the real local socket). +fn discover_podman_socket() -> Option> { + let output = Command::new("podman") + .args(["info", "--format", "json"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output() + .ok() + .filter(|o| o.status.success())?; + + let info: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?; + let is_remote = info["host"]["serviceIsRemote"].as_bool().unwrap_or(false); + + if is_remote { + discover_podman_machine_socket() + } else { + parse_podman_info_socket(&info) + } } -/// The Podman API socket symlink is not always present (it varies by -/// version, machine provider, and platform). Fall back to the CLI, -/// which has its own internal discovery for reaching the machine. -fn podman_cli_responds() -> bool { - Command::new("podman") - .arg("info") +/// Extract the socket path from `podman info` JSON output. +/// Used on native Linux where `remoteSocket.path` is the real local socket. +fn parse_podman_info_socket(info: &serde_json::Value) -> Option> { + let path_str = info["host"]["remoteSocket"]["path"].as_str()?; + let path = path_str.strip_prefix("unix://").unwrap_or(path_str); + if path.is_empty() { + return None; + } + Some(Some(PathBuf::from(path))) +} + +/// Run `podman machine inspect` to discover the host-side forwarded socket. +/// Used on macOS/Windows where the Podman service runs inside a VM. +fn discover_podman_machine_socket() -> Option> { + let output = Command::new("podman") + .args(["machine", "inspect", "--format", "json"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .output() - .is_ok_and(|o| o.status.success()) + .ok() + .filter(|o| o.status.success())?; + + let machines: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?; + parse_podman_machine_inspect(&machines) +} + +/// Extract the host-side socket path from `podman machine inspect` JSON. +fn parse_podman_machine_inspect(machines: &serde_json::Value) -> Option> { + let path_str = machines + .as_array() + .and_then(|arr| arr.first()) + .and_then(|m| m["ConnectionInfo"]["PodmanSocket"]["Path"].as_str())?; + if path_str.is_empty() { + return None; + } + Some(Some(PathBuf::from(path_str))) } fn podman_socket_candidates() -> Vec { @@ -741,8 +834,9 @@ mod tests { #[cfg(unix)] use super::is_reachable_unix_socket; use super::{ - ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, GatewayJwtConfig, detect_driver, - docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env, + ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, DetectedDriver, + GatewayJwtConfig, detect_driver, docker_host_unix_socket_path, is_unix_socket, + parse_podman_info_socket, parse_podman_machine_inspect, podman_socket_candidates_from_env, podman_socket_responds, }; #[cfg(unix)] @@ -970,7 +1064,13 @@ mod tests { } let result = detect_driver(); - assert_eq!(result, Some(ComputeDriverKind::Kubernetes)); + assert_eq!( + result, + Some(DetectedDriver { + kind: ComputeDriverKind::Kubernetes, + socket_path: None, + }) + ); // Restore the original env var unsafe { @@ -980,4 +1080,98 @@ mod tests { } } } + + #[test] + fn parse_podman_info_socket_extracts_linux_local_socket() { + let info: serde_json::Value = serde_json::json!({ + "host": { + "serviceIsRemote": false, + "remoteSocket": { + "path": "unix:///run/user/1000/podman/podman.sock", + "exists": true + } + } + }); + assert_eq!( + parse_podman_info_socket(&info), + Some(Some(PathBuf::from("/run/user/1000/podman/podman.sock"))) + ); + } + + #[test] + fn parse_podman_info_socket_handles_path_without_unix_prefix() { + let info: serde_json::Value = serde_json::json!({ + "host": { + "remoteSocket": { + "path": "/run/user/1000/podman/podman.sock", + "exists": true + } + } + }); + assert_eq!( + parse_podman_info_socket(&info), + Some(Some(PathBuf::from("/run/user/1000/podman/podman.sock"))) + ); + } + + #[test] + fn parse_podman_info_socket_returns_none_for_missing_path() { + let info: serde_json::Value = serde_json::json!({ + "host": { + "remoteSocket": {} + } + }); + assert_eq!(parse_podman_info_socket(&info), None); + } + + #[test] + fn parse_podman_info_socket_returns_none_for_empty_path() { + let info: serde_json::Value = serde_json::json!({ + "host": { + "remoteSocket": { + "path": "", + "exists": false + } + } + }); + assert_eq!(parse_podman_info_socket(&info), None); + } + + #[test] + fn parse_podman_machine_inspect_extracts_macos_socket() { + let machines: serde_json::Value = serde_json::json!([ + { + "ConnectionInfo": { + "PodmanSocket": { + "Path": "/var/folders/1q/jx7s14b928n8zvstgfk98lj00000gn/T/podman/podman-machine-default-api.sock" + }, + "PodmanPipe": null + }, + "Name": "podman-machine-default" + } + ]); + assert_eq!( + parse_podman_machine_inspect(&machines), + Some(Some(PathBuf::from( + "/var/folders/1q/jx7s14b928n8zvstgfk98lj00000gn/T/podman/podman-machine-default-api.sock" + ))) + ); + } + + #[test] + fn parse_podman_machine_inspect_returns_none_for_empty_array() { + let machines: serde_json::Value = serde_json::json!([]); + assert_eq!(parse_podman_machine_inspect(&machines), None); + } + + #[test] + fn parse_podman_machine_inspect_returns_none_for_missing_socket() { + let machines: serde_json::Value = serde_json::json!([ + { + "ConnectionInfo": {}, + "Name": "podman-machine-default" + } + ]); + assert_eq!(parse_podman_machine_inspect(&machines), None); + } } diff --git a/crates/openshell-core/src/lib.rs b/crates/openshell-core/src/lib.rs index c3241cdd8..f3980a4f9 100644 --- a/crates/openshell-core/src/lib.rs +++ b/crates/openshell-core/src/lib.rs @@ -28,8 +28,8 @@ pub mod telemetry; pub mod time; pub use config::{ - ComputeDriverKind, Config, GatewayAuthConfig, GatewayJwtConfig, MtlsAuthConfig, OidcConfig, - TlsConfig, + ComputeDriverKind, Config, DetectedDriver, GatewayAuthConfig, GatewayJwtConfig, + MtlsAuthConfig, OidcConfig, TlsConfig, }; pub use error::{ComputeDriverError, Error, Result}; pub use metadata::{GetResourceVersion, ObjectId, ObjectLabels, ObjectName, SetResourceVersion}; diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 748cec264..c03fed237 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -612,7 +612,7 @@ fn merge_file_into_args(args: &mut RunArgs, file: &GatewayFileSection, matches: fn effective_single_driver(args: &RunArgs) -> Option { match args.drivers.as_slice() { - [] => openshell_core::config::detect_driver(), + [] => openshell_core::config::detect_driver().map(|d| d.kind), [driver] => Some(*driver), _ => None, } diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 676e23071..db184df6a 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -44,7 +44,7 @@ pub mod tracing_bus; mod ws_tunnel; use metrics_exporter_prometheus::PrometheusBuilder; -use openshell_core::{ComputeDriverKind, Config, Error, Result}; +use openshell_core::{ComputeDriverKind, Config, DetectedDriver, Error, Result}; use std::collections::HashMap; use std::io::ErrorKind; use std::net::SocketAddr; @@ -703,11 +703,11 @@ async fn build_compute_runtime( tracing_log_bus: TracingLogBus, supervisor_sessions: Arc, ) -> Result { - let driver = configured_compute_driver(config)?; - info!(driver = %driver, "Using compute driver"); - warn_if_kubernetes_sandbox_jwt_expiry_disabled(config, driver); + let detected = configured_compute_driver(config)?; + info!(driver = %detected.kind, "Using compute driver"); + warn_if_kubernetes_sandbox_jwt_expiry_disabled(config, detected.kind); - match driver { + match detected.kind { ComputeDriverKind::Kubernetes => { let mut k8s = kubernetes_config_from_file(file)?; if let Ok(size) = std::env::var("OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE") { @@ -754,6 +754,8 @@ async fn build_compute_runtime( podman.gateway_port = config.bind_address.port(); if let Ok(p) = std::env::var("OPENSHELL_PODMAN_SOCKET") { podman.socket_path = PathBuf::from(p); + } else if let Some(discovered) = detected.socket_path { + podman.socket_path = discovered; } if let Ok(ip) = std::env::var("OPENSHELL_PODMAN_HOST_GATEWAY_IP") { podman.host_gateway_ip = ip; @@ -850,13 +852,16 @@ fn apply_podman_local_tls_defaults( Ok(()) } -fn configured_compute_driver(config: &Config) -> Result { +fn configured_compute_driver(config: &Config) -> Result { match config.compute_drivers.as_slice() { [] => match openshell_core::config::detect_driver() { - Some(ComputeDriverKind::Vm) => Err(Error::config( + Some(DetectedDriver { + kind: ComputeDriverKind::Vm, + .. + }) => Err(Error::config( "vm compute driver is opt-in only; set --drivers vm or OPENSHELL_DRIVERS=vm", )), - Some(driver) => Ok(driver), + Some(detected) => Ok(detected), None => Err(Error::config( "no compute driver configured and auto-detection found no suitable driver; \ set --drivers or OPENSHELL_DRIVERS to kubernetes, podman, docker, or vm", @@ -867,7 +872,10 @@ fn configured_compute_driver(config: &Config) -> Result { | ComputeDriverKind::Vm | ComputeDriverKind::Docker | ComputeDriverKind::Podman), - ] => Ok(*driver), + ] => Ok(DetectedDriver { + kind: *driver, + socket_path: None, + }), drivers => Err(Error::config(format!( "multiple compute drivers are not supported yet; configured drivers: {}", drivers @@ -905,7 +913,7 @@ mod tests { serve_gateway_listener, }; use openshell_core::{ - ComputeDriverKind, Config, + ComputeDriverKind, Config, DetectedDriver, proto::{HealthRequest, open_shell_client::OpenShellClient}, }; use rcgen::{CertificateParams, IsCa, KeyPair}; @@ -1246,15 +1254,15 @@ mod tests { let result = configured_compute_driver(&config); // Either we get a detected driver or an error about none being detected. match result { - Ok(driver) => { + Ok(detected) => { assert!( matches!( - driver, + detected.kind, ComputeDriverKind::Kubernetes | ComputeDriverKind::Docker | ComputeDriverKind::Podman ), - "auto-detected unexpected driver: {driver:?}" + "auto-detected unexpected driver: {detected:?}" ); } Err(e) => { @@ -1282,28 +1290,23 @@ mod tests { #[test] fn configured_compute_driver_accepts_podman() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Podman]); - assert_eq!( - configured_compute_driver(&config).unwrap(), - ComputeDriverKind::Podman - ); + let detected = configured_compute_driver(&config).unwrap(); + assert_eq!(detected.kind, ComputeDriverKind::Podman); + assert_eq!(detected.socket_path, None); } #[test] fn configured_compute_driver_accepts_vm() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Vm]); - assert_eq!( - configured_compute_driver(&config).unwrap(), - ComputeDriverKind::Vm - ); + let detected = configured_compute_driver(&config).unwrap(); + assert_eq!(detected.kind, ComputeDriverKind::Vm); } #[test] fn configured_compute_driver_accepts_docker() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Docker]); - assert_eq!( - configured_compute_driver(&config).unwrap(), - ComputeDriverKind::Docker - ); + let detected = configured_compute_driver(&config).unwrap(); + assert_eq!(detected.kind, ComputeDriverKind::Docker); } #[test] From 0adada1d5e32526ac833a79a81777c6e1289a7e7 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 12 Jun 2026 12:13:49 -0400 Subject: [PATCH 3/5] fix(core): drop --format json from podman machine inspect Unlike `podman info`, `podman machine inspect` outputs JSON by default. Passing `--format json` is interpreted as a Go template literal, causing it to output the string "json" instead of the JSON payload. Signed-off-by: Russell Bryant --- crates/openshell-core/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index b0ce32033..d84804f03 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -215,7 +215,7 @@ fn parse_podman_info_socket(info: &serde_json::Value) -> Option> /// Used on macOS/Windows where the Podman service runs inside a VM. fn discover_podman_machine_socket() -> Option> { let output = Command::new("podman") - .args(["machine", "inspect", "--format", "json"]) + .args(["machine", "inspect"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) From d1c89f5aaff2f60e1ee251f3fc49cb8eb9a99935 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 12 Jun 2026 12:18:16 -0400 Subject: [PATCH 4/5] fix(core): always return socket path from Podman detection When the socket probe succeeds against a well-known candidate, return that path so the driver uses the exact socket that was verified rather than rediscovering it via default_socket_path(). This ensures detection and driver connection always agree on the socket, regardless of whether it was found via probe or CLI discovery. Signed-off-by: Russell Bryant --- crates/openshell-core/src/config.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index d84804f03..243b3c726 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -97,9 +97,9 @@ impl FromStr for ComputeDriverKind { #[derive(Debug, Clone, PartialEq, Eq)] pub struct DetectedDriver { pub kind: ComputeDriverKind, - /// Socket path discovered during detection (Podman only, for now). - /// `None` when the socket was found via the static candidate list - /// (which the driver already knows about) or for non-Podman drivers. + /// API socket path discovered during detection (Podman only, for now). + /// Always populated for Podman so the driver connects to the exact + /// socket that detection verified. `None` for non-Podman drivers. pub socket_path: Option, } @@ -147,22 +147,20 @@ fn is_binary_available(name: &str) -> bool { .is_ok_and(|output| output.status.success()) } -/// Detect whether Podman is available and, when the socket is not at a -/// well-known path, discover the host-side API socket via the CLI. +/// Detect whether Podman is available and discover the API socket path. /// -/// Returns `Some(socket_path)` where `socket_path` is: -/// - `Some(path)` when the socket was discovered dynamically (CLI fallback), -/// - `None` when a static candidate already responded (the driver knows -/// those paths and will find it again). +/// Returns `Some(path)` when Podman is reachable — the path is always +/// populated so the driver uses the exact socket that detection verified. /// -/// Returns `None` (outer) when Podman is not available at all. +/// Returns `None` when Podman is not available at all. fn detect_podman() -> Option> { // Fast path: one of the well-known socket candidates responds. - if podman_socket_candidates() - .iter() - .any(|path| podman_socket_responds(path)) + // Return the path that worked so the driver doesn't have to rediscover it. + if let Some(path) = podman_socket_candidates() + .into_iter() + .find(|path| podman_socket_responds(path)) { - return Some(None); + return Some(Some(path)); } // Slow path: the socket symlink is missing or at a non-standard From 2ebf19c5d94c95111246fc98b4c08308c61eff73 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 12 Jun 2026 12:25:19 -0400 Subject: [PATCH 5/5] refactor(core): make DetectedDriver an enum with per-driver data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each variant carries only its own connection metadata — Podman gets a socket_path, other drivers carry nothing. Eliminates the generic Optional field and makes the match arms self-documenting. Signed-off-by: Russell Bryant --- crates/openshell-core/src/config.rs | 87 ++++++++++++++--------------- crates/openshell-server/src/cli.rs | 2 +- crates/openshell-server/src/lib.rs | 67 +++++++++++----------- 3 files changed, 79 insertions(+), 77 deletions(-) diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index 243b3c726..ab41aa7d1 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -92,15 +92,32 @@ impl FromStr for ComputeDriverKind { } } -/// Result of [`detect_driver`]: the driver kind plus any connection -/// metadata discovered during probing (e.g. the Podman API socket path). +/// Result of [`detect_driver`] or an explicitly configured driver, carrying +/// any driver-specific connection metadata discovered during probing. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct DetectedDriver { - pub kind: ComputeDriverKind, - /// API socket path discovered during detection (Podman only, for now). - /// Always populated for Podman so the driver connects to the exact - /// socket that detection verified. `None` for non-Podman drivers. - pub socket_path: Option, +pub enum DetectedDriver { + Kubernetes, + Docker, + /// VM is never auto-detected but flows through the same path when + /// explicitly configured via `--drivers vm`. + Vm, + Podman { + /// API socket path verified during detection. + socket_path: PathBuf, + }, +} + +impl DetectedDriver { + /// The [`ComputeDriverKind`] for logging and match guards. + #[must_use] + pub fn kind(&self) -> ComputeDriverKind { + match self { + Self::Kubernetes => ComputeDriverKind::Kubernetes, + Self::Docker => ComputeDriverKind::Docker, + Self::Vm => ComputeDriverKind::Vm, + Self::Podman { .. } => ComputeDriverKind::Podman, + } + } } /// Auto-detect the appropriate compute driver based on the runtime environment. @@ -113,27 +130,18 @@ pub struct DetectedDriver { pub fn detect_driver() -> Option { // Kubernetes: check for KUBERNETES_SERVICE_HOST env var (set inside pods) if std::env::var_os("KUBERNETES_SERVICE_HOST").is_some() { - return Some(DetectedDriver { - kind: ComputeDriverKind::Kubernetes, - socket_path: None, - }); + return Some(DetectedDriver::Kubernetes); } // Podman: check for a reachable local API socket, falling back to CLI // discovery which also resolves the host-side socket path. if let Some(socket_path) = detect_podman() { - return Some(DetectedDriver { - kind: ComputeDriverKind::Podman, - socket_path, - }); + return Some(DetectedDriver::Podman { socket_path }); } // Docker: check if the CLI is available or a local Docker socket exists. if is_docker_available() { - return Some(DetectedDriver { - kind: ComputeDriverKind::Docker, - socket_path: None, - }); + return Some(DetectedDriver::Docker); } None @@ -149,18 +157,15 @@ fn is_binary_available(name: &str) -> bool { /// Detect whether Podman is available and discover the API socket path. /// -/// Returns `Some(path)` when Podman is reachable — the path is always -/// populated so the driver uses the exact socket that detection verified. -/// -/// Returns `None` when Podman is not available at all. -fn detect_podman() -> Option> { +/// Returns the verified socket path, or `None` when Podman is not +/// available at all. +fn detect_podman() -> Option { // Fast path: one of the well-known socket candidates responds. - // Return the path that worked so the driver doesn't have to rediscover it. if let Some(path) = podman_socket_candidates() .into_iter() .find(|path| podman_socket_responds(path)) { - return Some(Some(path)); + return Some(path); } // Slow path: the socket symlink is missing or at a non-standard @@ -178,7 +183,7 @@ fn detect_podman() -> Option> { /// is the VM-internal path, which is not reachable from the host). /// 3. If `serviceIsRemote` is false, use `remoteSocket.path` directly /// (on native Linux this IS the real local socket). -fn discover_podman_socket() -> Option> { +fn discover_podman_socket() -> Option { let output = Command::new("podman") .args(["info", "--format", "json"]) .stdin(Stdio::null()) @@ -200,18 +205,18 @@ fn discover_podman_socket() -> Option> { /// Extract the socket path from `podman info` JSON output. /// Used on native Linux where `remoteSocket.path` is the real local socket. -fn parse_podman_info_socket(info: &serde_json::Value) -> Option> { +fn parse_podman_info_socket(info: &serde_json::Value) -> Option { let path_str = info["host"]["remoteSocket"]["path"].as_str()?; let path = path_str.strip_prefix("unix://").unwrap_or(path_str); if path.is_empty() { return None; } - Some(Some(PathBuf::from(path))) + Some(PathBuf::from(path)) } /// Run `podman machine inspect` to discover the host-side forwarded socket. /// Used on macOS/Windows where the Podman service runs inside a VM. -fn discover_podman_machine_socket() -> Option> { +fn discover_podman_machine_socket() -> Option { let output = Command::new("podman") .args(["machine", "inspect"]) .stdin(Stdio::null()) @@ -226,7 +231,7 @@ fn discover_podman_machine_socket() -> Option> { } /// Extract the host-side socket path from `podman machine inspect` JSON. -fn parse_podman_machine_inspect(machines: &serde_json::Value) -> Option> { +fn parse_podman_machine_inspect(machines: &serde_json::Value) -> Option { let path_str = machines .as_array() .and_then(|arr| arr.first()) @@ -234,7 +239,7 @@ fn parse_podman_machine_inspect(machines: &serde_json::Value) -> Option Vec { @@ -1062,13 +1067,7 @@ mod tests { } let result = detect_driver(); - assert_eq!( - result, - Some(DetectedDriver { - kind: ComputeDriverKind::Kubernetes, - socket_path: None, - }) - ); + assert_eq!(result, Some(DetectedDriver::Kubernetes)); // Restore the original env var unsafe { @@ -1092,7 +1091,7 @@ mod tests { }); assert_eq!( parse_podman_info_socket(&info), - Some(Some(PathBuf::from("/run/user/1000/podman/podman.sock"))) + Some(PathBuf::from("/run/user/1000/podman/podman.sock")) ); } @@ -1108,7 +1107,7 @@ mod tests { }); assert_eq!( parse_podman_info_socket(&info), - Some(Some(PathBuf::from("/run/user/1000/podman/podman.sock"))) + Some(PathBuf::from("/run/user/1000/podman/podman.sock")) ); } @@ -1150,9 +1149,9 @@ mod tests { ]); assert_eq!( parse_podman_machine_inspect(&machines), - Some(Some(PathBuf::from( + Some(PathBuf::from( "/var/folders/1q/jx7s14b928n8zvstgfk98lj00000gn/T/podman/podman-machine-default-api.sock" - ))) + )) ); } diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index c03fed237..719b90948 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -612,7 +612,7 @@ fn merge_file_into_args(args: &mut RunArgs, file: &GatewayFileSection, matches: fn effective_single_driver(args: &RunArgs) -> Option { match args.drivers.as_slice() { - [] => openshell_core::config::detect_driver().map(|d| d.kind), + [] => openshell_core::config::detect_driver().map(|d| d.kind()), [driver] => Some(*driver), _ => None, } diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index db184df6a..1be1416bf 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -704,11 +704,11 @@ async fn build_compute_runtime( supervisor_sessions: Arc, ) -> Result { let detected = configured_compute_driver(config)?; - info!(driver = %detected.kind, "Using compute driver"); - warn_if_kubernetes_sandbox_jwt_expiry_disabled(config, detected.kind); + info!(driver = %detected.kind(), "Using compute driver"); + warn_if_kubernetes_sandbox_jwt_expiry_disabled(config, detected.kind()); - match detected.kind { - ComputeDriverKind::Kubernetes => { + match detected { + DetectedDriver::Kubernetes => { let mut k8s = kubernetes_config_from_file(file)?; if let Ok(size) = std::env::var("OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE") { k8s.workspace_default_storage_size = size; @@ -724,7 +724,7 @@ async fn build_compute_runtime( .await .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } - ComputeDriverKind::Docker => ComputeRuntime::new_docker( + DetectedDriver::Docker => ComputeRuntime::new_docker( config.clone(), docker_config.clone(), store, @@ -735,7 +735,7 @@ async fn build_compute_runtime( ) .await .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))), - ComputeDriverKind::Vm => { + DetectedDriver::Vm => { let (channel, driver_process) = compute::vm::spawn(config, vm_config).await?; ComputeRuntime::new_remote_vm( channel, @@ -749,13 +749,13 @@ async fn build_compute_runtime( .await .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } - ComputeDriverKind::Podman => { + DetectedDriver::Podman { socket_path } => { let mut podman = podman_config_from_file(file)?; podman.gateway_port = config.bind_address.port(); if let Ok(p) = std::env::var("OPENSHELL_PODMAN_SOCKET") { podman.socket_path = PathBuf::from(p); - } else if let Some(discovered) = detected.socket_path { - podman.socket_path = discovered; + } else { + podman.socket_path = socket_path; } if let Ok(ip) = std::env::var("OPENSHELL_PODMAN_HOST_GATEWAY_IP") { podman.host_gateway_ip = ip; @@ -855,10 +855,7 @@ fn apply_podman_local_tls_defaults( fn configured_compute_driver(config: &Config) -> Result { match config.compute_drivers.as_slice() { [] => match openshell_core::config::detect_driver() { - Some(DetectedDriver { - kind: ComputeDriverKind::Vm, - .. - }) => Err(Error::config( + Some(DetectedDriver::Vm) => Err(Error::config( "vm compute driver is opt-in only; set --drivers vm or OPENSHELL_DRIVERS=vm", )), Some(detected) => Ok(detected), @@ -867,15 +864,7 @@ fn configured_compute_driver(config: &Config) -> Result { set --drivers or OPENSHELL_DRIVERS to kubernetes, podman, docker, or vm", )), }, - [ - driver @ (ComputeDriverKind::Kubernetes - | ComputeDriverKind::Vm - | ComputeDriverKind::Docker - | ComputeDriverKind::Podman), - ] => Ok(DetectedDriver { - kind: *driver, - socket_path: None, - }), + [driver] => explicit_driver(*driver), drivers => Err(Error::config(format!( "multiple compute drivers are not supported yet; configured drivers: {}", drivers @@ -887,6 +876,17 @@ fn configured_compute_driver(config: &Config) -> Result { } } +fn explicit_driver(kind: ComputeDriverKind) -> Result { + Ok(match kind { + ComputeDriverKind::Kubernetes => DetectedDriver::Kubernetes, + ComputeDriverKind::Docker => DetectedDriver::Docker, + ComputeDriverKind::Vm => DetectedDriver::Vm, + ComputeDriverKind::Podman => DetectedDriver::Podman { + socket_path: openshell_driver_podman::PodmanComputeConfig::default_socket_path(), + }, + }) +} + fn kubernetes_sandbox_jwt_expiry_disabled(config: &Config, driver: ComputeDriverKind) -> bool { matches!(driver, ComputeDriverKind::Kubernetes) && config @@ -1257,10 +1257,10 @@ mod tests { Ok(detected) => { assert!( matches!( - detected.kind, - ComputeDriverKind::Kubernetes - | ComputeDriverKind::Docker - | ComputeDriverKind::Podman + detected, + DetectedDriver::Kubernetes + | DetectedDriver::Docker + | DetectedDriver::Podman { .. } ), "auto-detected unexpected driver: {detected:?}" ); @@ -1291,22 +1291,25 @@ mod tests { fn configured_compute_driver_accepts_podman() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Podman]); let detected = configured_compute_driver(&config).unwrap(); - assert_eq!(detected.kind, ComputeDriverKind::Podman); - assert_eq!(detected.socket_path, None); + assert_eq!(detected.kind(), ComputeDriverKind::Podman); } #[test] fn configured_compute_driver_accepts_vm() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Vm]); - let detected = configured_compute_driver(&config).unwrap(); - assert_eq!(detected.kind, ComputeDriverKind::Vm); + assert_eq!( + configured_compute_driver(&config).unwrap(), + DetectedDriver::Vm + ); } #[test] fn configured_compute_driver_accepts_docker() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Docker]); - let detected = configured_compute_driver(&config).unwrap(); - assert_eq!(detected.kind, ComputeDriverKind::Docker); + assert_eq!( + configured_compute_driver(&config).unwrap(), + DetectedDriver::Docker + ); } #[test]