diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index ce7734262..b2088e63a 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. /// @@ -222,33 +222,29 @@ 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); } - 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!( @@ -267,7 +263,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) ) { @@ -318,14 +314,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), ); @@ -404,15 +400,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, + }) +} + +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)), + ); - if args.disable_tls { + 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"); @@ -421,22 +433,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 — \ @@ -444,17 +456,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 { @@ -691,87 +697,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, - &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, - &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}; @@ -1613,6 +1538,54 @@ 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![super::ComputeDriverKind::Podman] + ); + 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 @@ -1659,18 +1632,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..7a0edd6d3 --- /dev/null +++ b/crates/openshell-server/src/compute/driver_config.rs @@ -0,0 +1,314 @@ +// 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 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, +} + +/// 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, "kubernetes")?; + 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, "kubernetes") +} + +/// 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, "podman")?; + 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, "docker")?; + 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, "vm")?; + apply_vm_runtime_defaults(&mut cfg, context); + Ok(cfg) +} + +fn driver_config_from_context( + context: DriverStartupContext<'_>, + driver: ComputeDriverKind, + driver_name: &'static str, +) -> Result +where + T: Default + serde::de::DeserializeOwned, +{ + driver_config_from_file(context.file, driver, driver_name) +} + +fn driver_config_from_file( + file: Option<&config_file::ConfigFile>, + driver: ComputeDriverKind, + driver_name: &'static str, +) -> Result +where + T: Default + serde::de::DeserializeOwned, +{ + let Some(file) = file else { + return Ok(T::default()); + }; + let merged = config_file::driver_table( + driver, + &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_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::*; + + fn test_context(file: Option<&config_file::ConfigFile>) -> DriverStartupContext<'_> { + DriverStartupContext { + file, + guest_tls: None, + gateway_port: openshell_core::config::DEFAULT_SERVER_PORT, + gateway_tls_enabled: false, + } + } + + #[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 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 272c3907f..bb38523c7 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -3,10 +3,13 @@ //! Gateway-owned compute orchestration over a pluggable compute backend. +pub mod driver_config; 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; @@ -31,11 +34,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; @@ -257,7 +258,6 @@ impl ComputeRuntime { sandbox_watch_bus: SandboxWatchBus, tracing_log_bus: TracingLogBus, supervisor_sessions: Arc, - _allows_loopback_endpoints: bool, gateway_bind_addresses: Vec, ) -> Result { let default_image = driver @@ -323,7 +323,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - true, gateway_bind_addresses, ) .await @@ -340,7 +339,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, driver, @@ -352,7 +351,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - false, Vec::new(), ) .await @@ -379,7 +377,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - true, Vec::new(), ) .await @@ -408,7 +405,6 @@ impl ComputeRuntime { sandbox_watch_bus, tracing_log_bus, supervisor_sessions, - true, Vec::new(), ) .await diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index dda8708e0..1cdbb374c 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -50,7 +50,6 @@ use openshell_core::{ComputeDriverKind, Config, Error, Result}; 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}; @@ -62,17 +61,22 @@ 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; 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; 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 { @@ -198,13 +202,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")); @@ -233,11 +240,15 @@ 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(), + }; 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(), @@ -315,7 +326,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 { @@ -712,9 +724,7 @@ 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, @@ -723,16 +733,14 @@ async fn build_compute_runtime( ) -> Result { let driver = configured_compute_driver(config)?; info!(driver = %driver, "Using compute driver"); - warn_if_kubernetes_sandbox_jwt_expiry_disabled(config, driver); - match driver { + let runtime = match driver { 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, @@ -740,21 +748,23 @@ async fn build_compute_runtime( supervisor_sessions.clone(), ) .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } - 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}"))), + 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 + } ComputeDriverKind::Vm => { - let (channel, driver_process) = compute::vm::spawn(config, vm_config).await?; + let vm_config = compute::driver_config::vm_config_from_context(driver_startup)?; + let (channel, driver_process) = compute::vm::spawn(config, &vm_config).await?; ComputeRuntime::new_remote_vm( channel, Some(driver_process), @@ -765,21 +775,11 @@ async fn build_compute_runtime( supervisor_sessions, ) .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) } 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, @@ -787,85 +787,10 @@ 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, - &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(openshell_driver_podman::PodmanComputeConfig::default()); }; - let merged = config_file::driver_table( - ComputeDriverKind::Podman, - &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 openshell_driver_podman::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}"))) } fn configured_compute_driver(config: &Config) -> Result { @@ -897,16 +822,15 @@ fn configured_compute_driver(config: &Config) -> Result { } } -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" ); @@ -919,8 +843,7 @@ mod tests { 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, @@ -1296,7 +1219,7 @@ mod tests { } #[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 { @@ -1310,65 +1233,12 @@ mod tests { } 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 + &config_with_jwt_ttl(0) )); assert!(!kubernetes_sandbox_jwt_expiry_disabled( - &config_with_jwt_ttl(0), - ComputeDriverKind::Docker + &config_with_jwt_ttl(3600) )); - assert!(!kubernetes_sandbox_jwt_expiry_disabled( - &Config::new(None), - ComputeDriverKind::Kubernetes - )); - } - - #[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]