From ceab8da18899a421745efe12f3bbac704de97327 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Tue, 23 Jun 2026 20:22:14 +0200 Subject: [PATCH 1/4] refactor(server): remove unused compute runtime constructor parameter Signed-off-by: Evan Lezar --- crates/openshell-server/src/compute/mod.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 368c9c7e5..4e1308b35 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -292,7 +292,6 @@ impl ComputeRuntime { sandbox_watch_bus: SandboxWatchBus, tracing_log_bus: TracingLogBus, supervisor_sessions: Arc, - _allows_loopback_endpoints: bool, gateway_bind_addresses: Vec, ) -> Result { let capabilities = driver @@ -365,7 +364,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - true, gateway_bind_addresses, ) .await @@ -394,7 +392,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - false, Vec::new(), ) .await @@ -420,7 +417,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - true, Vec::new(), ) .await @@ -449,7 +445,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - true, Vec::new(), ) .await From df1198884a90d9860acb2b39f3723879c5a65ce8 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Tue, 23 Jun 2026 11:32:09 +0200 Subject: [PATCH 2/4] refactor(server): normalize compute driver type imports Signed-off-by: Evan Lezar --- crates/openshell-server/src/compute/mod.rs | 10 +++++----- crates/openshell-server/src/lib.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 4e1308b35..1a0785b68 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -7,6 +7,8 @@ pub mod lease; pub mod vm; pub use openshell_driver_docker::DockerComputeConfig; +pub use openshell_driver_kubernetes::KubernetesComputeConfig; +pub use openshell_driver_podman::PodmanComputeConfig; pub use vm::VmComputeConfig; use crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE; @@ -33,11 +35,9 @@ use openshell_core::proto::{ }; use openshell_driver_docker::DockerComputeDriver; use openshell_driver_kubernetes::{ - ComputeDriverService, KubernetesComputeConfig, KubernetesComputeDriver, -}; -use openshell_driver_podman::{ - ComputeDriverService as PodmanDriverService, PodmanComputeConfig, PodmanComputeDriver, + ComputeDriverService as KubernetesDriverService, KubernetesComputeDriver, }; +use openshell_driver_podman::{ComputeDriverService as PodmanDriverService, PodmanComputeDriver}; use prost::Message; use std::fmt; use std::net::SocketAddr; @@ -380,7 +380,7 @@ impl ComputeRuntime { let driver = KubernetesComputeDriver::new(config) .await .map_err(|err| ComputeError::Message(err.to_string()))?; - let driver: SharedComputeDriver = Arc::new(ComputeDriverService::new(driver)); + let driver: SharedComputeDriver = Arc::new(KubernetesDriverService::new(driver)); Self::from_driver( ComputeDriverKind::Kubernetes.as_str().to_string(), driver, diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index d0dbb1681..893cd09b5 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -69,11 +69,13 @@ use tracing::{debug, error, info, warn}; #[cfg(test)] pub(crate) static TEST_ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); -use compute::{ComputeRuntime, DockerComputeConfig, VmComputeConfig}; +use compute::{ + ComputeRuntime, DockerComputeConfig, KubernetesComputeConfig, PodmanComputeConfig, + VmComputeConfig, +}; pub use grpc::OpenShellService; pub use http::{health_router, http_router, metrics_router, service_http_router}; pub use multiplex::{MultiplexService, MultiplexedService}; -use openshell_driver_kubernetes::KubernetesComputeConfig; pub use persistence::Store; use sandbox_index::SandboxIndex; use sandbox_watch::SandboxWatchBus; @@ -858,11 +860,9 @@ fn kubernetes_config_for_k8s_sa_bootstrap( } /// Same pattern as [`kubernetes_config_from_file`] but for Podman. -fn podman_config_from_file( - file: Option<&config_file::ConfigFile>, -) -> Result { +fn podman_config_from_file(file: Option<&config_file::ConfigFile>) -> Result { let Some(file) = file else { - return Ok(openshell_driver_podman::PodmanComputeConfig::default()); + return Ok(PodmanComputeConfig::default()); }; let merged = config_file::driver_table( ComputeDriverKind::Podman, @@ -876,7 +876,7 @@ fn podman_config_from_file( fn apply_podman_local_tls_defaults( config: &Config, - podman: &mut openshell_driver_podman::PodmanComputeConfig, + podman: &mut PodmanComputeConfig, ) -> Result<()> { if config.tls.is_none() || podman.guest_tls_ca.is_some() From 7dd007747832739a658257ed82aa31c0f001c990 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Fri, 26 Jun 2026 14:49:21 +0200 Subject: [PATCH 3/4] refactor(server): key driver config tables by name Signed-off-by: Evan Lezar --- crates/openshell-server/src/cli.rs | 8 ++-- crates/openshell-server/src/config_file.rs | 49 ++++++++++++++++------ crates/openshell-server/src/lib.rs | 4 +- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index ef43dd405..4169c3405 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -761,7 +761,7 @@ fn build_vm_config( ) -> Result { let mut cfg = if let Some(file) = file { let merged = config_file::driver_table( - ComputeDriverKind::Vm, + ComputeDriverKind::Vm.as_str(), &file.openshell.gateway, file.openshell.drivers.get("vm"), ); @@ -796,7 +796,7 @@ fn build_docker_config( ) -> Result { let mut cfg = if let Some(file) = file { let merged = config_file::driver_table( - ComputeDriverKind::Docker, + ComputeDriverKind::Docker.as_str(), &file.openshell.gateway, file.openshell.drivers.get("docker"), ); @@ -1807,7 +1807,7 @@ namespace = "agents" "#, ); let merged = crate::config_file::driver_table( - super::ComputeDriverKind::Kubernetes, + super::ComputeDriverKind::Kubernetes.as_str(), &file.openshell.gateway, file.openshell.drivers.get("kubernetes"), ); @@ -1830,7 +1830,7 @@ default_image = "k8s-specific:1.0" "#, ); let merged = crate::config_file::driver_table( - super::ComputeDriverKind::Kubernetes, + super::ComputeDriverKind::Kubernetes.as_str(), &file.openshell.gateway, file.openshell.drivers.get("kubernetes"), ); diff --git a/crates/openshell-server/src/config_file.rs b/crates/openshell-server/src/config_file.rs index 3875756dc..b65b5f3b0 100644 --- a/crates/openshell-server/src/config_file.rs +++ b/crates/openshell-server/src/config_file.rs @@ -231,7 +231,7 @@ pub fn load(path: &Path) -> Result { /// the gateway section, which keeps each driver's `deny_unknown_fields` /// invariant intact. pub fn driver_table( - driver: ComputeDriverKind, + driver_name: &str, gateway: &GatewayFileSection, raw: Option<&toml::Value>, ) -> toml::Value { @@ -240,7 +240,7 @@ pub fn driver_table( _ => toml::Table::new(), }; - for key in inheritable_keys(driver) { + for key in inheritable_keys(driver_name) { if merged.contains_key(*key) { continue; } @@ -255,9 +255,9 @@ pub fn driver_table( /// Inheritance allowlist (the Q4 "high-overlap set"). Each driver opts in /// to a specific subset so a gateway-wide default does not accidentally land /// in a driver table that does not understand the field. -fn inheritable_keys(driver: ComputeDriverKind) -> &'static [&'static str] { - match driver { - ComputeDriverKind::Kubernetes => &[ +fn inheritable_keys(driver_name: &str) -> &'static [&'static str] { + match driver_name.parse::().ok() { + Some(ComputeDriverKind::Kubernetes) => &[ "namespace", "default_image", "supervisor_image", @@ -267,7 +267,7 @@ fn inheritable_keys(driver: ComputeDriverKind) -> &'static [&'static str] { "enable_user_namespaces", "sa_token_ttl_secs", ], - ComputeDriverKind::Docker => &[ + Some(ComputeDriverKind::Docker) => &[ "sandbox_namespace", "default_image", "supervisor_image", @@ -276,7 +276,7 @@ fn inheritable_keys(driver: ComputeDriverKind) -> &'static [&'static str] { "guest_tls_cert", "guest_tls_key", ], - ComputeDriverKind::Podman => &[ + Some(ComputeDriverKind::Podman) => &[ "default_image", "supervisor_image", "host_gateway_ip", @@ -284,12 +284,13 @@ fn inheritable_keys(driver: ComputeDriverKind) -> &'static [&'static str] { "guest_tls_cert", "guest_tls_key", ], - ComputeDriverKind::Vm => &[ + Some(ComputeDriverKind::Vm) => &[ "default_image", "guest_tls_ca", "guest_tls_cert", "guest_tls_key", ], + None => &[], } } @@ -484,7 +485,7 @@ version = 2 namespace = "agents" }; let merged = driver_table( - ComputeDriverKind::Kubernetes, + ComputeDriverKind::Kubernetes.as_str(), &gateway, Some(&toml::Value::Table(raw)), ); @@ -511,7 +512,7 @@ version = 2 host_gateway_ip: Some("10.0.0.1".to_string()), ..Default::default() }; - let merged = driver_table(ComputeDriverKind::Docker, &gateway, None); + let merged = driver_table(ComputeDriverKind::Docker.as_str(), &gateway, None); let table = merged.as_table().expect("table"); assert_eq!( table.get("sandbox_namespace").and_then(|v| v.as_str()), @@ -534,7 +535,7 @@ version = 2 host_gateway_ip: Some("192.168.127.254".to_string()), ..Default::default() }; - let merged = driver_table(ComputeDriverKind::Podman, &gateway, None); + let merged = driver_table(ComputeDriverKind::Podman.as_str(), &gateway, None); let table = merged.as_table().expect("table"); assert_eq!( table.get("default_image").and_then(|v| v.as_str()), @@ -556,7 +557,7 @@ version = 2 default_image = "driver-specific" }; let merged = driver_table( - ComputeDriverKind::Podman, + ComputeDriverKind::Podman.as_str(), &gateway, Some(&toml::Value::Table(raw)), ); @@ -578,7 +579,7 @@ version = 2 client_tls_secret_name: Some("openshell-sandbox-tls".to_string()), ..Default::default() }; - let merged = driver_table(ComputeDriverKind::Docker, &gateway, None); + let merged = driver_table(ComputeDriverKind::Docker.as_str(), &gateway, None); assert!( !merged .as_table() @@ -587,6 +588,28 @@ version = 2 ); } + #[test] + fn remote_driver_table_does_not_inherit_gateway_defaults() { + let gateway = GatewayFileSection { + default_image: Some("gateway-default:1.0".to_string()), + host_gateway_ip: Some("10.0.0.1".to_string()), + ..Default::default() + }; + let raw = toml::toml! { + socket_path = "/run/openshell/kyma.sock" + }; + + let merged = driver_table("kyma", &gateway, Some(&toml::Value::Table(raw))); + let table = merged.as_table().expect("table"); + + assert_eq!( + table.get("socket_path").and_then(|v| v.as_str()), + Some("/run/openshell/kyma.sock") + ); + assert!(!table.contains_key("default_image")); + assert!(!table.contains_key("host_gateway_ip")); + } + #[test] fn missing_path_is_io_error() { let err = load(Path::new("/nonexistent/openshell-gateway.toml")) diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 893cd09b5..3c1e036d9 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -834,7 +834,7 @@ fn kubernetes_config_from_file( return Ok(KubernetesComputeConfig::default()); }; let merged = config_file::driver_table( - ComputeDriverKind::Kubernetes, + ComputeDriverKind::Kubernetes.as_str(), &file.openshell.gateway, file.openshell.drivers.get("kubernetes"), ); @@ -865,7 +865,7 @@ fn podman_config_from_file(file: Option<&config_file::ConfigFile>) -> Result Date: Fri, 26 Jun 2026 15:00:31 +0200 Subject: [PATCH 4/4] refactor(server): normalize compute driver config acquisition Signed-off-by: Evan Lezar --- crates/openshell-server/src/cli.rs | 224 ++++------ .../src/compute/driver_config.rs | 420 ++++++++++++++++++ crates/openshell-server/src/compute/mod.rs | 1 + crates/openshell-server/src/lib.rs | 396 +++++------------ 4 files changed, 621 insertions(+), 420 deletions(-) create mode 100644 crates/openshell-server/src/compute/driver_config.rs diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 4169c3405..9aee2bc6d 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -14,10 +14,10 @@ use tracing::{info, warn}; use tracing_subscriber::EnvFilter; use crate::certgen; -use crate::compute::{DockerComputeConfig, VmComputeConfig}; +use crate::compute::driver_config::GuestTlsPaths; use crate::config_file::{self, ConfigFile, GatewayFileSection}; use crate::defaults::{self, LocalTlsPaths}; -use crate::{run_server, tracing_bus::TracingLogBus}; +use crate::{ServerStartupConfig, run_server, tracing_bus::TracingLogBus}; /// `OpenShell` gateway process - gRPC and HTTP server with protocol multiplexing. /// @@ -232,34 +232,30 @@ pub async fn run_cli() -> Result<()> { } } -async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { +fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result { // Load TOML when explicitly requested, or from the default XDG location // when that file exists. Missing default config is not an error: runtime // defaults and OPENSHELL_* env vars are enough for package-managed starts. - let config_path = resolve_config_path(&args)?; + let config_path = resolve_config_path(args)?; let file: Option = if let Some(path) = config_path { Some(config_file::load(&path).map_err(|e| miette::miette!("{e}"))?) } else { None }; if let Some(file) = file.as_ref() { - merge_file_into_args(&mut args, &file.openshell.gateway, &matches); + merge_file_into_args(args, &file.openshell.gateway, matches); } - normalize_compute_driver_socket_args(&mut args, &matches)?; + normalize_compute_driver_socket_args(args, matches)?; - let local_tls = apply_runtime_defaults(&mut args)?; + let local_tls = apply_runtime_defaults(args)?; + let guest_tls = local_tls.as_ref().map(GuestTlsPaths::from); let local_jwt = defaults::complete_local_jwt_config()?; - let tracing_log_bus = TracingLogBus::new(); - tracing_log_bus.install_subscriber( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)), - ); - let bind = SocketAddr::new(args.bind_address, args.port); let has_client_ca = args.tls_client_ca.is_some(); let has_oidc = args.oidc_issuer.is_some(); - let mtls_auth_enabled = resolve_mtls_auth_enabled(&args, &matches, file.as_ref()); + let mtls_auth_enabled = resolve_mtls_auth_enabled(args, matches, file.as_ref()); if args.disable_tls && has_client_ca { return Err(miette::miette!( @@ -278,7 +274,7 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { } if mtls_auth_enabled && matches!( - effective_single_driver(&args), + effective_single_driver(args), Some(ComputeDriverKind::Kubernetes) ) { @@ -329,14 +325,14 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { let health_bind = resolve_aux_listener( args.bind_address, args.health_port, - &matches, + matches, "health_port", || file_gateway.and_then(|g| g.health_bind_address), ); let metrics_bind = resolve_aux_listener( args.bind_address, args.metrics_port, - &matches, + matches, "metrics_port", || file_gateway.and_then(|g| g.metrics_bind_address), ); @@ -422,15 +418,31 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { config.gateway_jwt = Some(jwt); } - let vm_config = build_vm_config( - file.as_ref(), - local_tls.as_ref(), - args.disable_tls, - args.port, - )?; - let docker_config = build_docker_config(file.as_ref(), local_tls.as_ref())?; + Ok(ServerStartupConfig { + config, + config_file: file, + guest_tls, + }) +} - if args.disable_tls { +async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { + let prepared = prepare_server_config(&mut args, &matches)?; + + let tracing_log_bus = TracingLogBus::new(); + tracing_log_bus.install_subscriber( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new(&prepared.config.log_level)), + ); + + let has_client_ca = prepared + .config + .tls + .as_ref() + .and_then(|tls| tls.client_ca_path.as_ref()) + .is_some(); + let has_oidc = prepared.config.oidc.is_some(); + + if prepared.config.tls.is_none() { warn!("TLS disabled — listening on plaintext HTTP"); } else { info!("TLS enabled — listening on encrypted HTTPS"); @@ -439,22 +451,22 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { if has_client_ca { info!("TLS client certificate verification enabled"); } - if config.mtls_auth.enabled { + if prepared.config.mtls_auth.enabled { info!("mTLS user authentication enabled"); } if has_oidc { info!("OIDC authentication enabled"); } - if config.auth.allow_unauthenticated_users { + if prepared.config.auth.allow_unauthenticated_users { warn!( "Unauthenticated user access enabled — only use this for trusted local development or a fully trusted fronting proxy" ); } - if !config.auth.allow_unauthenticated_users - && !config.mtls_auth.enabled + if !prepared.config.auth.allow_unauthenticated_users + && !prepared.config.mtls_auth.enabled && !has_oidc - && config.gateway_jwt.is_none() + && prepared.config.gateway_jwt.is_none() { warn!( "Neither mTLS user auth nor OIDC nor sandbox JWT auth is configured — \ @@ -462,17 +474,11 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { ); } - info!(bind = %config.bind_address, "Starting OpenShell server"); + info!(bind = %prepared.config.bind_address, "Starting OpenShell server"); - Box::pin(run_server( - config, - vm_config, - docker_config, - file, - tracing_log_bus, - )) - .await - .into_diagnostic() + Box::pin(run_server(prepared, tracing_log_bus)) + .await + .into_diagnostic() } fn parse_compute_driver(value: &str) -> std::result::Result { @@ -751,87 +757,6 @@ fn resolve_mtls_auth_enabled( is_singleplayer_driver(args) } -/// Build [`VmComputeConfig`] from the `[openshell.drivers.vm]` table -/// inherited from `[openshell.gateway]`. -fn build_vm_config( - file: Option<&ConfigFile>, - local_tls: Option<&LocalTlsPaths>, - disable_tls: bool, - gateway_port: u16, -) -> Result { - let mut cfg = if let Some(file) = file { - let merged = config_file::driver_table( - ComputeDriverKind::Vm.as_str(), - &file.openshell.gateway, - file.openshell.drivers.get("vm"), - ); - merged - .try_into::() - .map_err(|e| miette::miette!("invalid [openshell.drivers.vm] table: {e}"))? - } else { - VmComputeConfig::default() - }; - - if cfg.state_dir.as_os_str().is_empty() { - cfg.state_dir = VmComputeConfig::default_state_dir(); - } - if cfg.grpc_endpoint.trim().is_empty() && (disable_tls || local_tls.is_some()) { - let scheme = if disable_tls { "http" } else { "https" }; - cfg.grpc_endpoint = format!("{scheme}://127.0.0.1:{gateway_port}"); - } - apply_guest_tls_defaults( - &mut cfg.guest_tls_ca, - &mut cfg.guest_tls_cert, - &mut cfg.guest_tls_key, - local_tls, - ); - Ok(cfg) -} - -/// Build [`DockerComputeConfig`] using the same inheritance pattern as -/// [`build_vm_config`]. -fn build_docker_config( - file: Option<&ConfigFile>, - local_tls: Option<&LocalTlsPaths>, -) -> Result { - let mut cfg = if let Some(file) = file { - let merged = config_file::driver_table( - ComputeDriverKind::Docker.as_str(), - &file.openshell.gateway, - file.openshell.drivers.get("docker"), - ); - merged - .try_into::() - .map_err(|e| miette::miette!("invalid [openshell.drivers.docker] table: {e}"))? - } else { - DockerComputeConfig::default() - }; - apply_guest_tls_defaults( - &mut cfg.guest_tls_ca, - &mut cfg.guest_tls_cert, - &mut cfg.guest_tls_key, - local_tls, - ); - Ok(cfg) -} - -fn apply_guest_tls_defaults( - ca: &mut Option, - cert: &mut Option, - key: &mut Option, - local_tls: Option<&LocalTlsPaths>, -) { - if ca.is_none() - && cert.is_none() - && key.is_none() - && let Some(paths) = local_tls - { - *ca = Some(paths.ca.clone()); - *cert = Some(paths.client_cert.clone()); - *key = Some(paths.client_key.clone()); - } -} - #[cfg(test)] mod tests { use super::{Cli, command}; @@ -1793,6 +1718,51 @@ enable_loopback_service_http = false ); } + #[test] + fn server_config_preparation_ignores_unselected_driver_tables() { + let _lock = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let state = tempfile::tempdir().unwrap(); + let local_tls = tempfile::tempdir().unwrap(); + let _g1 = EnvVarGuard::set("XDG_STATE_HOME", state.path().to_str().unwrap()); + let _g2 = EnvVarGuard::set( + "OPENSHELL_LOCAL_TLS_DIR", + local_tls.path().to_str().unwrap(), + ); + let config_path = state.path().join("gateway.toml"); + std::fs::write( + &config_path, + r#" +[openshell.drivers.docker] +unknown_docker_key = true + +[openshell.drivers.vm] +mem_mib = "not-a-number" +"#, + ) + .unwrap(); + + let (mut args, matches) = parse_with_args(&[ + "openshell-gateway", + "--config", + config_path.to_str().unwrap(), + "--db-url", + "sqlite::memory:", + "--drivers", + "podman", + "--disable-tls", + ]); + + let prepared = + super::prepare_server_config(&mut args, &matches).expect("server config is prepared"); + + assert_eq!(prepared.config.compute_drivers, vec!["podman".to_string()]); + let file = prepared.config_file.expect("config file is preserved"); + assert!(file.openshell.drivers.contains_key("docker")); + assert!(file.openshell.drivers.contains_key("vm")); + } + #[test] fn driver_inherits_shared_image_from_gateway_section() { // [openshell.gateway].default_image inherits into the K8s driver @@ -1839,18 +1809,4 @@ default_image = "k8s-specific:1.0" .expect("deserializes"); assert_eq!(parsed.default_image, "k8s-specific:1.0"); } - - #[test] - fn docker_config_reads_bind_mount_opt_in_from_driver_table() { - let file = config_file_from_toml( - r" -[openshell.drivers.docker] -enable_bind_mounts = true -", - ); - - let cfg = super::build_docker_config(Some(&file), None).expect("docker config"); - - assert!(cfg.enable_bind_mounts); - } } diff --git a/crates/openshell-server/src/compute/driver_config.rs b/crates/openshell-server/src/compute/driver_config.rs new file mode 100644 index 000000000..e3e78acab --- /dev/null +++ b/crates/openshell-server/src/compute/driver_config.rs @@ -0,0 +1,420 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Selected compute-driver config construction. +//! +//! This module owns loading the selected driver config from TOML, applying +//! driver-specific environment overrides, and applying gateway startup defaults. +//! It does not acquire, connect to, or start compute drivers. + +use crate::config_file; +use crate::defaults::LocalTlsPaths; +use openshell_core::{ComputeDriverKind, Error, Result}; +use openshell_driver_docker::DockerComputeConfig; +use openshell_driver_kubernetes::KubernetesComputeConfig; +use openshell_driver_podman::PodmanComputeConfig; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::path::PathBuf; + +use super::VmComputeConfig; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GuestTlsPaths { + ca: PathBuf, + cert: PathBuf, + key: PathBuf, +} + +impl From<&LocalTlsPaths> for GuestTlsPaths { + fn from(paths: &LocalTlsPaths) -> Self { + Self { + ca: paths.ca.clone(), + cert: paths.client_cert.clone(), + key: paths.client_key.clone(), + } + } +} + +#[derive(Clone, Copy)] +pub struct DriverStartupContext<'a> { + pub file: Option<&'a config_file::ConfigFile>, + pub guest_tls: Option<&'a GuestTlsPaths>, + pub gateway_port: u16, + pub gateway_tls_enabled: bool, + pub endpoint_overrides: &'a BTreeMap, +} + +/// Build the selected Kubernetes config from TOML plus runtime defaults. +pub fn kubernetes_config_from_context( + context: DriverStartupContext<'_>, +) -> Result { + let mut cfg = driver_config_from_context(context, ComputeDriverKind::Kubernetes.as_str())?; + apply_kubernetes_runtime_defaults(&mut cfg); + Ok(cfg) +} + +pub fn kubernetes_config_for_k8s_sa_bootstrap( + file: Option<&config_file::ConfigFile>, +) -> Result { + let Some(file) = file else { + return Err(Error::config( + "K8s ServiceAccount bootstrap requires [openshell.drivers.kubernetes] when sandbox JWT issuing is enabled in-cluster", + )); + }; + if !file.openshell.drivers.contains_key("kubernetes") { + return Err(Error::config( + "K8s ServiceAccount bootstrap requires [openshell.drivers.kubernetes] when sandbox JWT issuing is enabled in-cluster", + )); + } + driver_config_from_file(Some(file), ComputeDriverKind::Kubernetes.as_str()) +} + +/// Build the selected Podman config from TOML plus runtime defaults. +pub fn podman_config_from_context( + context: DriverStartupContext<'_>, +) -> Result { + let mut podman = driver_config_from_context(context, ComputeDriverKind::Podman.as_str())?; + apply_podman_runtime_defaults(&mut podman, context); + Ok(podman) +} + +/// Build the selected Docker config from TOML plus runtime defaults. +pub fn docker_config_from_context( + context: DriverStartupContext<'_>, +) -> Result { + let mut cfg = driver_config_from_context(context, ComputeDriverKind::Docker.as_str())?; + apply_docker_runtime_defaults(&mut cfg, context); + Ok(cfg) +} + +/// Build the selected VM config from TOML plus runtime defaults. +pub fn vm_config_from_context(context: DriverStartupContext<'_>) -> Result { + let mut cfg = driver_config_from_context(context, ComputeDriverKind::Vm.as_str())?; + apply_vm_runtime_defaults(&mut cfg, context); + Ok(cfg) +} + +pub fn remote_driver_config_from_context( + context: DriverStartupContext<'_>, + name: &str, +) -> Result { + let mut cfg = driver_config_from_context(context, name)?; + apply_remote_driver_overrides(&mut cfg, context, name); + validate_remote_driver_config(&cfg, name)?; + Ok(cfg) +} + +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct RemoteDriverConfig { + #[serde(default)] + pub socket_path: PathBuf, +} + +fn driver_config_from_context(context: DriverStartupContext<'_>, driver_name: &str) -> Result +where + T: Default + serde::de::DeserializeOwned, +{ + driver_config_from_file(context.file, driver_name) +} + +fn driver_config_from_file( + file: Option<&config_file::ConfigFile>, + driver_name: &str, +) -> Result +where + T: Default + serde::de::DeserializeOwned, +{ + let Some(file) = file else { + return Ok(T::default()); + }; + let merged = config_file::driver_table( + driver_name, + &file.openshell.gateway, + file.openshell.drivers.get(driver_name), + ); + merged.try_into().map_err(|e| { + Error::config(format!( + "invalid [openshell.drivers.{driver_name}] table: {e}" + )) + }) +} + +fn apply_kubernetes_runtime_defaults(k8s: &mut KubernetesComputeConfig) { + if let Ok(size) = std::env::var("OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE") { + k8s.workspace_default_storage_size = size; + } +} + +fn apply_podman_runtime_defaults( + podman: &mut PodmanComputeConfig, + context: DriverStartupContext<'_>, +) { + podman.gateway_port = context.gateway_port; + apply_podman_env_overrides(podman); + apply_guest_tls_defaults_to_split_fields( + &mut podman.guest_tls_ca, + &mut podman.guest_tls_cert, + &mut podman.guest_tls_key, + context.guest_tls, + ); +} + +fn apply_docker_runtime_defaults(cfg: &mut DockerComputeConfig, context: DriverStartupContext<'_>) { + apply_guest_tls_defaults_to_split_fields( + &mut cfg.guest_tls_ca, + &mut cfg.guest_tls_cert, + &mut cfg.guest_tls_key, + context.guest_tls, + ); +} + +fn apply_vm_runtime_defaults(cfg: &mut VmComputeConfig, context: DriverStartupContext<'_>) { + if cfg.state_dir.as_os_str().is_empty() { + cfg.state_dir = VmComputeConfig::default_state_dir(); + } + if cfg.grpc_endpoint.trim().is_empty() + && (!context.gateway_tls_enabled || context.guest_tls.is_some()) + { + let scheme = if context.gateway_tls_enabled { + "https" + } else { + "http" + }; + cfg.grpc_endpoint = format!("{scheme}://127.0.0.1:{}", context.gateway_port); + } + + apply_guest_tls_defaults_to_split_fields( + &mut cfg.guest_tls_ca, + &mut cfg.guest_tls_cert, + &mut cfg.guest_tls_key, + context.guest_tls, + ); +} + +fn apply_podman_env_overrides(podman: &mut PodmanComputeConfig) { + if let Ok(p) = std::env::var("OPENSHELL_PODMAN_SOCKET") { + podman.socket_path = PathBuf::from(p); + } + if let Ok(ip) = std::env::var("OPENSHELL_PODMAN_HOST_GATEWAY_IP") { + podman.host_gateway_ip = ip; + } +} + +fn apply_remote_driver_overrides( + cfg: &mut RemoteDriverConfig, + context: DriverStartupContext<'_>, + name: &str, +) { + if let Some(socket_path) = context.endpoint_overrides.get(name) { + cfg.socket_path = socket_path.clone(); + } +} + +fn validate_remote_driver_config(cfg: &RemoteDriverConfig, name: &str) -> Result<()> { + if !cfg.socket_path.as_os_str().is_empty() { + return Ok(()); + } + Err(Error::config(format!( + "remote compute driver '{name}' requires socket_path" + ))) +} + +fn apply_guest_tls_defaults_to_split_fields( + ca: &mut Option, + cert: &mut Option, + key: &mut Option, + defaults: Option<&GuestTlsPaths>, +) { + if ca.is_none() + && cert.is_none() + && key.is_none() + && let Some(paths) = defaults + { + *ca = Some(paths.ca.clone()); + *cert = Some(paths.cert.clone()); + *key = Some(paths.key.clone()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + + fn test_context(file: Option<&config_file::ConfigFile>) -> DriverStartupContext<'_> { + static EMPTY_ENDPOINT_OVERRIDES: std::sync::LazyLock> = + std::sync::LazyLock::new(BTreeMap::new); + test_context_with_endpoint_overrides(file, &EMPTY_ENDPOINT_OVERRIDES) + } + + fn test_context_with_endpoint_overrides<'a>( + file: Option<&'a config_file::ConfigFile>, + endpoint_overrides: &'a BTreeMap, + ) -> DriverStartupContext<'a> { + DriverStartupContext { + file, + guest_tls: None, + gateway_port: openshell_core::config::DEFAULT_SERVER_PORT, + gateway_tls_enabled: false, + endpoint_overrides, + } + } + + #[test] + fn k8s_sa_bootstrap_rejects_missing_kubernetes_driver_config() { + let err = kubernetes_config_for_k8s_sa_bootstrap(None).unwrap_err(); + assert!(err.to_string().contains("[openshell.drivers.kubernetes]")); + + let file: config_file::ConfigFile = + toml::from_str("[openshell.gateway]\n").expect("valid config"); + let err = kubernetes_config_for_k8s_sa_bootstrap(Some(&file)).unwrap_err(); + assert!(err.to_string().contains("[openshell.drivers.kubernetes]")); + } + + #[test] + fn k8s_sa_bootstrap_uses_configured_namespace_and_service_account() { + let file: config_file::ConfigFile = toml::from_str( + r#" +[openshell.gateway] + +[openshell.drivers.kubernetes] +namespace = "sandboxes" +service_account_name = "sandbox-sa" +"#, + ) + .expect("valid config"); + + let cfg = kubernetes_config_for_k8s_sa_bootstrap(Some(&file)).unwrap(); + assert_eq!(cfg.namespace, "sandboxes"); + assert_eq!(cfg.service_account_name, "sandbox-sa"); + } + + #[test] + fn podman_config_reads_bind_mount_opt_in_from_driver_table() { + let file: config_file::ConfigFile = toml::from_str( + r" +[openshell.drivers.podman] +enable_bind_mounts = true +", + ) + .expect("valid config"); + + let cfg = podman_config_from_context(test_context(Some(&file))).expect("podman config"); + + assert!(cfg.enable_bind_mounts); + } + + #[test] + fn docker_config_reads_bind_mount_opt_in_from_driver_table() { + let file: config_file::ConfigFile = toml::from_str( + r" +[openshell.drivers.docker] +enable_bind_mounts = true +", + ) + .expect("valid config"); + + let cfg = docker_config_from_context(test_context(Some(&file))).expect("docker config"); + + assert!(cfg.enable_bind_mounts); + } + + #[test] + fn remote_driver_config_reads_socket_path_from_named_table() { + let file: config_file::ConfigFile = toml::from_str( + r#" +[openshell.drivers.kyma] +socket_path = "/run/openshell/kyma.sock" +"#, + ) + .expect("valid config"); + + let cfg = remote_driver_config_from_context(test_context(Some(&file)), "kyma") + .expect("remote config"); + + assert_eq!(cfg.socket_path, PathBuf::from("/run/openshell/kyma.sock")); + } + + #[test] + fn remote_driver_config_uses_endpoint_override_without_file() { + let endpoint_overrides = + BTreeMap::from([("kyma".to_string(), PathBuf::from("/tmp/kyma.sock"))]); + + let cfg = remote_driver_config_from_context( + test_context_with_endpoint_overrides(None, &endpoint_overrides), + "kyma", + ) + .expect("remote config"); + + assert_eq!(cfg.socket_path, PathBuf::from("/tmp/kyma.sock")); + } + + #[test] + fn remote_driver_config_endpoint_override_wins_over_file() { + let file: config_file::ConfigFile = toml::from_str( + r#" +[openshell.drivers.kyma] +socket_path = "/run/openshell/kyma.sock" +"#, + ) + .expect("valid config"); + let endpoint_overrides = + BTreeMap::from([("kyma".to_string(), PathBuf::from("/tmp/kyma.sock"))]); + + let cfg = remote_driver_config_from_context( + test_context_with_endpoint_overrides(Some(&file), &endpoint_overrides), + "kyma", + ) + .expect("remote config"); + + assert_eq!(cfg.socket_path, PathBuf::from("/tmp/kyma.sock")); + } + + #[test] + fn remote_driver_config_rejects_missing_socket_path() { + let err = remote_driver_config_from_context(test_context(None), "kyma").unwrap_err(); + + assert!( + err.to_string() + .contains("remote compute driver 'kyma' requires socket_path") + ); + } + + #[test] + fn docker_config_reports_selected_invalid_driver_table() { + let file: config_file::ConfigFile = toml::from_str( + r" +[openshell.drivers.docker] +unknown_docker_key = true +", + ) + .expect("valid config"); + + let err = docker_config_from_context(test_context(Some(&file))).unwrap_err(); + + assert!( + err.to_string() + .contains("invalid [openshell.drivers.docker] table") + ); + } + + #[test] + fn vm_config_reports_selected_invalid_driver_table() { + let file: config_file::ConfigFile = toml::from_str( + r#" +[openshell.drivers.vm] +mem_mib = "not-a-number" +"#, + ) + .expect("valid config"); + + let err = vm_config_from_context(test_context(Some(&file))).unwrap_err(); + + assert!( + err.to_string() + .contains("invalid [openshell.drivers.vm] table") + ); + } +} diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 1a0785b68..fec29f0c4 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -3,6 +3,7 @@ //! Gateway-owned compute orchestration over a pluggable compute backend. +pub mod driver_config; pub mod lease; pub mod vm; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 3c1e036d9..13f5c647c 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -53,11 +53,9 @@ mod ws_tunnel; use metrics_exporter_prometheus::PrometheusBuilder; use openshell_core::{ComputeDriverKind, Config, Error, Result}; -use serde::Deserialize; use std::collections::HashMap; use std::io::ErrorKind; use std::net::SocketAddr; -use std::path::PathBuf; #[cfg(test)] use std::sync::LazyLock; use std::sync::{Arc, Mutex}; @@ -69,10 +67,7 @@ use tracing::{debug, error, info, warn}; #[cfg(test)] pub(crate) static TEST_ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); -use compute::{ - ComputeRuntime, DockerComputeConfig, KubernetesComputeConfig, PodmanComputeConfig, - VmComputeConfig, -}; +use compute::ComputeRuntime; pub use grpc::OpenShellService; pub use http::{health_router, http_router, metrics_router, service_http_router}; pub use multiplex::{MultiplexService, MultiplexedService}; @@ -82,6 +77,12 @@ use sandbox_watch::SandboxWatchBus; pub use tls::TlsAcceptor; use tracing_bus::TracingLogBus; +pub(crate) struct ServerStartupConfig { + pub config: Config, + pub config_file: Option, + pub guest_tls: Option, +} + /// Server state shared across handlers. #[derive(Debug)] pub struct ServerState { @@ -207,13 +208,16 @@ impl ServerState { /// # Errors /// /// Returns an error if the server fails to start or encounters a fatal error. -pub async fn run_server( - config: Config, - vm_config: VmComputeConfig, - docker_config: DockerComputeConfig, - config_file: Option, +pub(crate) async fn run_server( + startup: ServerStartupConfig, tracing_log_bus: TracingLogBus, ) -> Result<()> { + let ServerStartupConfig { + config, + config_file, + guest_tls, + } = startup; + let database_url = config.database_url.trim(); if database_url.is_empty() { return Err(Error::config("database_url is required")); @@ -242,11 +246,16 @@ pub async fn run_server( let sandbox_index = SandboxIndex::new(); let sandbox_watch_bus = SandboxWatchBus::new(); let supervisor_sessions = Arc::new(supervisor_session::SupervisorSessionRegistry::new()); + let driver_startup = compute::driver_config::DriverStartupContext { + file: config_file.as_ref(), + guest_tls: guest_tls.as_ref(), + gateway_port: config.bind_address.port(), + gateway_tls_enabled: config.tls.is_some(), + endpoint_overrides: &config.compute_driver_endpoints, + }; let compute = build_compute_runtime( &config, - &vm_config, - &docker_config, - config_file.as_ref(), + driver_startup, store.clone(), sandbox_index.clone(), sandbox_watch_bus.clone(), @@ -324,7 +333,8 @@ pub async fn run_server( if state.sandbox_jwt_issuer.is_some() && std::env::var_os("KUBERNETES_SERVICE_HOST").is_some() { // Pod lookups and TokenReview identity checks must match the sandbox // namespace and service account used by the Kubernetes driver. - let kubernetes_config = kubernetes_config_for_k8s_sa_bootstrap(config_file.as_ref())?; + let kubernetes_config = + compute::driver_config::kubernetes_config_for_k8s_sa_bootstrap(config_file.as_ref())?; let sandbox_namespace = kubernetes_config.namespace; let sandbox_service_account = kubernetes_config.service_account_name; match kube::Client::try_default().await { @@ -721,29 +731,23 @@ async fn terminate_signal() { #[allow(clippy::too_many_arguments)] async fn build_compute_runtime( config: &Config, - vm_config: &VmComputeConfig, - docker_config: &DockerComputeConfig, - file: Option<&config_file::ConfigFile>, + driver_startup: compute::driver_config::DriverStartupContext<'_>, store: Arc, sandbox_index: SandboxIndex, sandbox_watch_bus: SandboxWatchBus, tracing_log_bus: TracingLogBus, supervisor_sessions: Arc, ) -> Result { - let driver = configured_compute_driver(config, file)?; + let driver = configured_compute_driver(config, driver_startup)?; info!(driver = %driver.name(), "Using compute driver"); - if let ConfiguredComputeDriver::Builtin(kind) = &driver { - warn_if_kubernetes_sandbox_jwt_expiry_disabled(config, *kind); - } - match driver { + let runtime = match driver { ConfiguredComputeDriver::Builtin(ComputeDriverKind::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; - } + warn_if_kubernetes_sandbox_jwt_expiry_disabled(config); + let k8s_config = + compute::driver_config::kubernetes_config_from_context(driver_startup)?; ComputeRuntime::new_kubernetes( - k8s, + k8s_config, store, sandbox_index, sandbox_watch_bus, @@ -751,32 +755,24 @@ async fn build_compute_runtime( supervisor_sessions.clone(), ) .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Docker) => ComputeRuntime::new_docker( - config.clone(), - docker_config.clone(), - store, - sandbox_index, - sandbox_watch_bus, - tracing_log_bus, - supervisor_sessions, - ) - .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))), + ConfiguredComputeDriver::Builtin(ComputeDriverKind::Docker) => { + let docker_config = compute::driver_config::docker_config_from_context(driver_startup)?; + ComputeRuntime::new_docker( + config.clone(), + docker_config, + store, + sandbox_index, + sandbox_watch_bus, + tracing_log_bus, + supervisor_sessions, + ) + .await + } ConfiguredComputeDriver::Builtin(ComputeDriverKind::Podman) => { - 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); - } - if let Ok(ip) = std::env::var("OPENSHELL_PODMAN_HOST_GATEWAY_IP") { - podman.host_gateway_ip = ip; - } - apply_podman_local_tls_defaults(config, &mut podman)?; - + let podman_config = compute::driver_config::podman_config_from_context(driver_startup)?; ComputeRuntime::new_podman( - podman, + podman_config, store, sandbox_index, sandbox_watch_bus, @@ -784,10 +780,10 @@ async fn build_compute_runtime( supervisor_sessions, ) .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } ConfiguredComputeDriver::Builtin(ComputeDriverKind::Vm) => { - let endpoint = compute::vm::spawn(config, vm_config).await?; + let vm_config = compute::driver_config::vm_config_from_context(driver_startup)?; + let endpoint = compute::vm::spawn(config, &vm_config).await?; ComputeRuntime::new_remote_driver( endpoint, store, @@ -797,16 +793,16 @@ async fn build_compute_runtime( supervisor_sessions, ) .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } - ConfiguredComputeDriver::Remote(remote) => { - let RemoteComputeDriverSelection { name, socket_path } = remote; + ConfiguredComputeDriver::Remote { name } => { + let remote_config = + compute::driver_config::remote_driver_config_from_context(driver_startup, &name)?; info!( driver = %name, - socket = %socket_path.display(), + socket = %remote_config.socket_path.display(), "Using remote compute driver endpoint" ); - let endpoint = compute::connect_remote_compute_driver(name, &socket_path) + let endpoint = compute::connect_remote_compute_driver(name, &remote_config.socket_path) .await .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))?; ComputeRuntime::new_remote_driver( @@ -818,115 +814,30 @@ async fn build_compute_runtime( supervisor_sessions, ) .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } - } -} - -/// Build a [`KubernetesComputeConfig`] from the file's -/// `[openshell.drivers.kubernetes]` table merged with inheritable -/// `[openshell.gateway]` defaults. Falls back to the driver's `Default` -/// when no file is present. -fn kubernetes_config_from_file( - file: Option<&config_file::ConfigFile>, -) -> Result { - let Some(file) = file else { - return Ok(KubernetesComputeConfig::default()); - }; - let merged = config_file::driver_table( - ComputeDriverKind::Kubernetes.as_str(), - &file.openshell.gateway, - file.openshell.drivers.get("kubernetes"), - ); - merged - .try_into() - .map_err(|e| Error::config(format!("invalid [openshell.drivers.kubernetes] table: {e}"))) -} - -fn kubernetes_config_for_k8s_sa_bootstrap( - file: Option<&config_file::ConfigFile>, -) -> Result { - let Some(file) = file else { - return Err(Error::config( - "K8s ServiceAccount bootstrap requires [openshell.drivers.kubernetes] when sandbox JWT issuing is enabled in-cluster", - )); }; - if !file.openshell.drivers.contains_key("kubernetes") { - return Err(Error::config( - "K8s ServiceAccount bootstrap requires [openshell.drivers.kubernetes] when sandbox JWT issuing is enabled in-cluster", - )); - } - kubernetes_config_from_file(Some(file)) -} -/// Same pattern as [`kubernetes_config_from_file`] but for Podman. -fn podman_config_from_file(file: Option<&config_file::ConfigFile>) -> Result { - let Some(file) = file else { - return Ok(PodmanComputeConfig::default()); - }; - let merged = config_file::driver_table( - ComputeDriverKind::Podman.as_str(), - &file.openshell.gateway, - file.openshell.drivers.get("podman"), - ); - merged - .try_into() - .map_err(|e| Error::config(format!("invalid [openshell.drivers.podman] table: {e}"))) -} - -fn apply_podman_local_tls_defaults( - config: &Config, - podman: &mut PodmanComputeConfig, -) -> Result<()> { - if config.tls.is_none() - || podman.guest_tls_ca.is_some() - || podman.guest_tls_cert.is_some() - || podman.guest_tls_key.is_some() - { - return Ok(()); - } - - let Some(paths) = defaults::complete_local_tls_paths() - .map_err(|e| Error::config(format!("failed to resolve local TLS defaults: {e}")))? - else { - return Ok(()); - }; - podman.guest_tls_ca = Some(paths.ca); - podman.guest_tls_cert = Some(paths.client_cert); - podman.guest_tls_key = Some(paths.client_key); - Ok(()) + runtime.map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } #[derive(Debug, Clone)] enum ConfiguredComputeDriver { Builtin(ComputeDriverKind), - Remote(RemoteComputeDriverSelection), + Remote { name: String }, } impl ConfiguredComputeDriver { fn name(&self) -> &str { match self { Self::Builtin(kind) => kind.as_str(), - Self::Remote(remote) => &remote.name, + Self::Remote { name } => name, } } } -#[derive(Debug, Clone)] -struct RemoteComputeDriverSelection { - name: String, - socket_path: PathBuf, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -struct RemoteComputeDriverConfig { - socket_path: PathBuf, -} - fn configured_compute_driver( config: &Config, - file: Option<&config_file::ConfigFile>, + driver_startup: compute::driver_config::DriverStartupContext<'_>, ) -> Result { match config.compute_drivers.as_slice() { [] => match openshell_core::config::detect_driver() { @@ -939,7 +850,7 @@ fn configured_compute_driver( set --drivers or OPENSHELL_DRIVERS to kubernetes, podman, docker, or vm", )), }, - [driver] => resolve_configured_compute_driver(driver, config, file), + [driver] => resolve_configured_compute_driver(driver, driver_startup), drivers => Err(Error::config(format!( "multiple compute drivers are not supported yet; configured drivers: {}", drivers.join(",") @@ -949,75 +860,37 @@ fn configured_compute_driver( fn resolve_configured_compute_driver( driver_name: &str, - config: &Config, - file: Option<&config_file::ConfigFile>, + driver_startup: compute::driver_config::DriverStartupContext<'_>, ) -> Result { let name = openshell_core::config::normalize_compute_driver_name(driver_name) .map_err(Error::config)?; let driver_kind = builtin_compute_driver(&name); - if let Some(socket_path) = config.compute_driver_endpoints.get(&name) { - if driver_kind.is_some() { - return Err(Error::config(format!( - "compute driver '{name}' is a reserved built-in driver and cannot be selected with a socket endpoint" - ))); - } - return Ok(ConfiguredComputeDriver::Remote( - RemoteComputeDriverSelection { - name, - socket_path: socket_path.clone(), - }, - )); + if driver_kind.is_some() && driver_startup.endpoint_overrides.contains_key(&name) { + return Err(Error::config(format!( + "compute driver '{name}' is a reserved built-in driver and cannot be selected with a socket endpoint" + ))); } if let Some(kind) = driver_kind { return Ok(ConfiguredComputeDriver::Builtin(kind)); } - let socket_path = remote_driver_socket_from_file(&name, file)?; - Ok(ConfiguredComputeDriver::Remote( - RemoteComputeDriverSelection { name, socket_path }, - )) + Ok(ConfiguredComputeDriver::Remote { name }) } fn builtin_compute_driver(name: &str) -> Option { name.parse().ok() } -fn remote_driver_socket_from_file( - name: &str, - file: Option<&config_file::ConfigFile>, -) -> Result { - let Some(file) = file else { - return Err(Error::config(format!( - "compute driver '{name}' is not a built-in driver; configure [openshell.drivers.{name}].socket_path or pass --drivers {name} with --compute-driver-socket" - ))); - }; - let Some(raw) = file.openshell.drivers.get(name) else { - return Err(Error::config(format!( - "compute driver '{name}' is not a built-in driver; configure [openshell.drivers.{name}].socket_path" - ))); - }; - let config = raw - .clone() - .try_into::() - .map_err(|err| { - Error::config(format!( - "invalid [openshell.drivers.{name}] table for remote compute driver: {err}" - )) - })?; - Ok(config.socket_path) -} - -fn kubernetes_sandbox_jwt_expiry_disabled(config: &Config, driver: ComputeDriverKind) -> bool { - matches!(driver, ComputeDriverKind::Kubernetes) - && config - .gateway_jwt - .as_ref() - .is_some_and(|jwt| jwt.ttl_secs == 0) +fn kubernetes_sandbox_jwt_expiry_disabled(config: &Config) -> bool { + config + .gateway_jwt + .as_ref() + .is_some_and(|jwt| jwt.ttl_secs == 0) } -fn warn_if_kubernetes_sandbox_jwt_expiry_disabled(config: &Config, driver: ComputeDriverKind) { - if kubernetes_sandbox_jwt_expiry_disabled(config, driver) { +fn warn_if_kubernetes_sandbox_jwt_expiry_disabled(config: &Config) { + if kubernetes_sandbox_jwt_expiry_disabled(config) { warn!( "Kubernetes gateway configured with non-expiring sandbox JWTs (gateway_jwt.ttl_secs = 0); set ttl_secs > 0 for shared Kubernetes deployments" ); @@ -1030,8 +903,7 @@ mod tests { ConfiguredComputeDriver, ConnectionProtocol, MultiplexService, ServerState, TlsAcceptor, allow_plaintext_service_http, classify_initial_bytes, configured_compute_driver, gateway_listener_addresses, is_benign_tls_handshake_failure, - kubernetes_config_for_k8s_sa_bootstrap, kubernetes_sandbox_jwt_expiry_disabled, - serve_gateway_listener, + kubernetes_sandbox_jwt_expiry_disabled, serve_gateway_listener, }; use openshell_core::{ ComputeDriverKind, Config, @@ -1039,7 +911,6 @@ mod tests { }; use std::io::{Error, ErrorKind}; use std::net::SocketAddr; - use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tempfile::{TempDir, tempdir}; @@ -1049,6 +920,19 @@ mod tests { use crate::tls_test_utils::{generate_test_certs_with_ca, install_rustls_provider}; + fn test_driver_startup<'a>( + config: &'a Config, + file: Option<&'a super::config_file::ConfigFile>, + ) -> crate::compute::driver_config::DriverStartupContext<'a> { + crate::compute::driver_config::DriverStartupContext { + file, + guest_tls: None, + gateway_port: openshell_core::config::DEFAULT_SERVER_PORT, + gateway_tls_enabled: false, + endpoint_overrides: &config.compute_driver_endpoints, + } + } + fn test_tls_acceptor() -> (TempDir, TlsAcceptor) { install_rustls_provider(); @@ -1344,7 +1228,7 @@ mod tests { // Empty drivers triggers auto-detection, which may return Some or None // depending on the environment. This test verifies the auto-detection path // is taken rather than immediately returning an error. - let result = configured_compute_driver(&config, None); + let result = configured_compute_driver(&config, test_driver_startup(&config, None)); // Either we get a detected driver or an error about none being detected. match result { Ok(ConfiguredComputeDriver::Builtin(driver)) => { @@ -1358,8 +1242,8 @@ mod tests { "auto-detected unexpected driver: {driver:?}" ); } - Ok(ConfiguredComputeDriver::Remote(remote)) => { - panic!("auto-detection returned remote driver: {remote:?}"); + Ok(ConfiguredComputeDriver::Remote { name }) => { + panic!("auto-detection returned remote driver: {name}"); } Err(e) => { assert!( @@ -1375,7 +1259,8 @@ mod tests { fn configured_compute_driver_rejects_multiple_entries() { let config = Config::new(None) .with_compute_drivers([ComputeDriverKind::Kubernetes, ComputeDriverKind::Podman]); - let err = configured_compute_driver(&config, None).unwrap_err(); + let err = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap_err(); assert!( err.to_string() .contains("multiple compute drivers are not supported yet") @@ -1386,7 +1271,8 @@ mod tests { #[test] fn configured_compute_driver_accepts_podman() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Podman]); - let driver = configured_compute_driver(&config, None).unwrap(); + let driver = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); assert!(matches!( driver, ConfiguredComputeDriver::Builtin(ComputeDriverKind::Podman) @@ -1396,7 +1282,8 @@ mod tests { #[test] fn configured_compute_driver_accepts_vm() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Vm]); - let driver = configured_compute_driver(&config, None).unwrap(); + let driver = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); assert!(matches!( driver, ConfiguredComputeDriver::Builtin(ComputeDriverKind::Vm) @@ -1406,7 +1293,8 @@ mod tests { #[test] fn configured_compute_driver_accepts_docker() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Docker]); - let driver = configured_compute_driver(&config, None).unwrap(); + let driver = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); assert!(matches!( driver, ConfiguredComputeDriver::Builtin(ComputeDriverKind::Docker) @@ -1414,28 +1302,15 @@ mod tests { } #[test] - fn configured_compute_driver_resolves_named_remote_from_file() { - let file: super::config_file::ConfigFile = toml::from_str( - r#" -[openshell.gateway] -compute_drivers = ["kyma"] - -[openshell.drivers.kyma] -socket_path = "/run/openshell/kyma.sock" -"#, - ) - .unwrap(); + fn configured_compute_driver_resolves_named_remote() { let config = Config::new(None).with_compute_drivers(["kyma"]); - let driver = configured_compute_driver(&config, Some(&file)).unwrap(); + let driver = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); match driver { - ConfiguredComputeDriver::Remote(remote) => { - assert_eq!(remote.name, "kyma"); - assert_eq!( - remote.socket_path, - PathBuf::from("/run/openshell/kyma.sock") - ); + ConfiguredComputeDriver::Remote { name } => { + assert_eq!(name, "kyma"); } ConfiguredComputeDriver::Builtin(other) => { panic!("expected remote driver, got builtin driver {other:?}") @@ -1449,7 +1324,8 @@ socket_path = "/run/openshell/kyma.sock" .with_compute_drivers([ComputeDriverKind::Vm]) .with_compute_driver_endpoint("vm", "/run/openshell/vm.sock"); - let err = configured_compute_driver(&config, None).unwrap_err(); + let err = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap_err(); assert!( err.to_string() @@ -1464,7 +1340,8 @@ socket_path = "/run/openshell/kyma.sock" .with_compute_drivers([ComputeDriverKind::Docker]) .with_compute_driver_endpoint("docker", "/run/openshell/docker.sock"); - let err = configured_compute_driver(&config, None).unwrap_err(); + let err = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap_err(); assert!( err.to_string() @@ -1474,7 +1351,7 @@ socket_path = "/run/openshell/kyma.sock" } #[test] - fn kubernetes_sandbox_jwt_expiry_disabled_warns_only_for_kubernetes_zero_ttl() { + fn kubernetes_sandbox_jwt_expiry_disabled_warns_for_zero_ttl() { fn config_with_jwt_ttl(ttl_secs: u64) -> Config { let mut config = Config::new(None); config.gateway_jwt = Some(openshell_core::GatewayJwtConfig { @@ -1488,65 +1365,12 @@ socket_path = "/run/openshell/kyma.sock" } assert!(kubernetes_sandbox_jwt_expiry_disabled( - &config_with_jwt_ttl(0), - ComputeDriverKind::Kubernetes - )); - assert!(!kubernetes_sandbox_jwt_expiry_disabled( - &config_with_jwt_ttl(3600), - ComputeDriverKind::Kubernetes - )); - assert!(!kubernetes_sandbox_jwt_expiry_disabled( - &config_with_jwt_ttl(0), - ComputeDriverKind::Docker + &config_with_jwt_ttl(0) )); assert!(!kubernetes_sandbox_jwt_expiry_disabled( - &Config::new(None), - ComputeDriverKind::Kubernetes + &config_with_jwt_ttl(3600) )); - } - - #[test] - fn k8s_sa_bootstrap_rejects_missing_kubernetes_driver_config() { - let err = kubernetes_config_for_k8s_sa_bootstrap(None).unwrap_err(); - assert!(err.to_string().contains("[openshell.drivers.kubernetes]")); - - let file: crate::config_file::ConfigFile = - toml::from_str("[openshell.gateway]\n").expect("valid config"); - let err = kubernetes_config_for_k8s_sa_bootstrap(Some(&file)).unwrap_err(); - assert!(err.to_string().contains("[openshell.drivers.kubernetes]")); - } - - #[test] - fn k8s_sa_bootstrap_uses_configured_namespace_and_service_account() { - let file: crate::config_file::ConfigFile = toml::from_str( - r#" -[openshell.gateway] - -[openshell.drivers.kubernetes] -namespace = "sandboxes" -service_account_name = "sandbox-sa" -"#, - ) - .expect("valid config"); - - let cfg = kubernetes_config_for_k8s_sa_bootstrap(Some(&file)).unwrap(); - assert_eq!(cfg.namespace, "sandboxes"); - assert_eq!(cfg.service_account_name, "sandbox-sa"); - } - - #[test] - fn podman_config_reads_bind_mount_opt_in_from_driver_table() { - let file: crate::config_file::ConfigFile = toml::from_str( - r" -[openshell.drivers.podman] -enable_bind_mounts = true -", - ) - .expect("valid config"); - - let cfg = crate::podman_config_from_file(Some(&file)).expect("podman config"); - - assert!(cfg.enable_bind_mounts); + assert!(!kubernetes_sandbox_jwt_expiry_disabled(&Config::new(None))); } #[test]