From 64c20da6b8f250caa0c8b06b3388b5715a0efd9b Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 18 Aug 2026 00:47:59 -0700 Subject: [PATCH] refactor(compute): register compiled drivers Signed-off-by: Drew Newberry --- .../skills/debug-openshell-cluster/SKILL.md | 7 + architecture/compute-runtimes.md | 17 + crates/openshell-core/src/config.rs | 8 +- crates/openshell-server/src/cli.rs | 20 +- .../src/compute/driver_config.rs | 11 +- crates/openshell-server/src/compute/mod.rs | 7 +- crates/openshell-server/src/lib.rs | 700 ++++++++++++++---- crates/openshell-server/src/main.rs | 5 +- 8 files changed, 622 insertions(+), 153 deletions(-) diff --git a/.agents/skills/debug-openshell-cluster/SKILL.md b/.agents/skills/debug-openshell-cluster/SKILL.md index bc35e0d351..d7723fd8d7 100644 --- a/.agents/skills/debug-openshell-cluster/SKILL.md +++ b/.agents/skills/debug-openshell-cluster/SKILL.md @@ -19,6 +19,13 @@ The target deployment flow is: 4. The CLI registers a reachable gateway endpoint with `openshell gateway add`. 5. The gateway creates sandboxes through the selected compute driver. +The standard gateway binary explicitly installs its compiled Docker, Podman, +Kubernetes, and VM registrations at startup. With no configured driver, the +gateway probes only installed registrations in priority order (Kubernetes, +Podman, then Docker); VM has no probe and remains opt-in. A custom gateway +binary may install a different set, so confirm the binary's registered drivers +when auto-detection reports that no suitable driver is available. + For local evaluation only, TLS may be disabled and the gateway can be reached through `http://127.0.0.1:`. ## Prerequisites diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index 3085745304..4b305ff7c0 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -91,6 +91,23 @@ The gateway records driver identity and version from the startup capability response. Elevated gateway info reports that initialized driver snapshot instead of re-querying drivers on each request. +## Compiled Driver Selection + +The gateway binary explicitly installs the compute drivers compiled into that +binary before entering server startup. The server selects a configured driver +by normalized registry name. When no driver is configured, it evaluates only +the installed drivers' probes and chooses the lowest registered priority. +Drivers without a probe, including VM, remain opt-in. + +This follows the same composition model as SQLx's `Any` drivers: the binary +defines the available implementation set, while the runtime consumes a generic +registry. Adding or removing a compiled driver therefore changes registration +rather than the server's selection flow. Alternate gateway binaries can install +their own `ComputeDriverFactory` registrations and hand the completed registry +to `run_cli_with_compute_drivers`; factories receive merged driver config and +finish through the same in-process runtime adapter. A configured UDS endpoint +still takes precedence over a compiled registration with the same name. + ## Stop and Start Lifecycle The gateway persists lifecycle intent before mutating compute: diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index fcbdeb73b6..62411e20b5 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -208,7 +208,9 @@ pub fn detect_driver() -> Option { None } -fn is_podman_available() -> bool { +/// Return whether a responsive local Podman API socket is available. +#[must_use] +pub fn is_podman_available() -> bool { detect_podman_socket().is_some() } @@ -266,7 +268,9 @@ fn podman_socket_candidates_from_env( candidates } -fn is_docker_available() -> bool { +/// Return whether a responsive local Docker API socket is available. +#[must_use] +pub fn is_docker_available() -> bool { detect_docker_socket().is_some() } diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 02afb3917f..36badb7182 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -17,7 +17,10 @@ use crate::certgen; use crate::compute::driver_config::GuestTlsPaths; use crate::config_file::{self, ConfigFile, GatewayFileSection}; use crate::defaults::{self, LocalTlsPaths}; -use crate::{ServerStartupConfig, run_server, tracing_bus::TracingLogBus}; +use crate::{ + ComputeDriverRegistry, ServerStartupConfig, install_default_compute_drivers, run_server, + tracing_bus::TracingLogBus, +}; /// `OpenShell` gateway process - gRPC and HTTP server with protocol multiplexing. /// @@ -220,6 +223,11 @@ pub fn command() -> Command { } pub async fn run_cli() -> Result<()> { + run_cli_with_compute_drivers(install_default_compute_drivers()).await +} + +/// Run the gateway CLI with the compute drivers linked by the binary. +pub async fn run_cli_with_compute_drivers(compute_drivers: ComputeDriverRegistry) -> Result<()> { rustls::crypto::ring::default_provider() .install_default() .map_err(|e| miette::miette!("failed to install rustls crypto provider: {e:?}"))?; @@ -229,7 +237,7 @@ pub async fn run_cli() -> Result<()> { match cli.command { Some(Commands::GenerateCerts(args)) => certgen::run(args).await, - None => Box::pin(run_from_args(cli.run, matches)).await, + None => Box::pin(run_from_args(cli.run, matches, compute_drivers)).await, } } @@ -469,7 +477,11 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result Result<()> { +async fn run_from_args( + mut args: RunArgs, + matches: ArgMatches, + compute_drivers: ComputeDriverRegistry, +) -> Result<()> { let prepared = prepare_server_config(&mut args, &matches)?; let tracing_log_bus = TracingLogBus::new(); @@ -538,7 +550,7 @@ async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> { info!(bind = %prepared.config.bind_address, "Starting OpenShell server"); - let result = Box::pin(run_server(prepared, tracing_log_bus)).await; + let result = Box::pin(run_server(prepared, tracing_log_bus, compute_drivers)).await; tracing_handle.shutdown(); diff --git a/crates/openshell-server/src/compute/driver_config.rs b/crates/openshell-server/src/compute/driver_config.rs index 9f4cac9a01..f0eb6f98b4 100644 --- a/crates/openshell-server/src/compute/driver_config.rs +++ b/crates/openshell-server/src/compute/driver_config.rs @@ -24,6 +24,12 @@ pub struct GuestTlsPaths { key: PathBuf, } +impl GuestTlsPaths { + pub(crate) fn as_paths(&self) -> (&std::path::Path, &std::path::Path, &std::path::Path) { + (&self.ca, &self.cert, &self.key) + } +} + impl From<&LocalTlsPaths> for GuestTlsPaths { fn from(paths: &LocalTlsPaths) -> Self { Self { @@ -60,7 +66,10 @@ pub struct RemoteDriverConfig { pub socket_path: PathBuf, } -fn driver_config_from_context(context: DriverStartupContext<'_>, driver_name: &str) -> Result +pub fn driver_config_from_context( + context: DriverStartupContext<'_>, + driver_name: &str, +) -> Result where T: Default + serde::de::DeserializeOwned, { diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index dfc1acf9aa..6027f80271 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -77,8 +77,9 @@ use tonic::{Code, Request, Status}; use tower::service_fn; use tracing::{Instrument as _, debug, info, warn}; -type DriverWatchStream = Pin> + Send>>; -type SharedComputeDriver = +pub type DriverWatchStream = + Pin> + Send>>; +pub type SharedComputeDriver = Arc + Send + Sync>; use traced_driver::TracedDriver; @@ -585,7 +586,7 @@ impl ComputeRuntime { driver.name = %driver_name, ) )] - async fn from_driver( + pub(crate) async fn from_driver( driver_name: String, driver: SharedComputeDriver, driver_process: Option>, diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 9ea8161a0d..2678ef6121 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -9,19 +9,9 @@ //! - Protocol multiplexing (gRPC + HTTP on same port) //! - mTLS support //! -//! TODO(driver-abstraction): `build_compute_runtime` still switches on -//! built-in driver names and calls driver-specific constructors -//! ([`ComputeRuntime::new_kubernetes`], [`ComputeRuntime::new_docker`], -//! [`compute::vm::spawn`] + [`ComputeRuntime::new_remote_driver`], -//! [`ComputeRuntime::new_podman`]). Endpoint-backed drivers now share the -//! remote `compute_driver.proto` path, so new remote drivers should enter -//! through named endpoint acquisition rather than gateway-wide socket side -//! channels. Once we have a generalized compute-driver registry, the remaining -//! per-arm wiring here should collapse to driver construction records that -//! produce either an in-process `SharedComputeDriver` or an acquired remote -//! endpoint, then hand the rest of the gateway a uniform [`ComputeRuntime`]. -//! The VM launch plumbing now lives in [`compute::vm`]; keep this file limited -//! to selecting and acquiring drivers. +//! Compiled-in compute drivers are installed into a registry at gateway +//! startup. Runtime selection only consults that registry or a configured +//! external endpoint; it does not switch on driver names. mod auth; pub mod certgen; @@ -58,8 +48,10 @@ mod tracing_setup; mod ws_tunnel; use metrics_exporter_prometheus::PrometheusBuilder; +#[cfg(target_os = "windows")] +use openshell_core::ComputeDriverKind; use openshell_core::net::set_tcp_nodelay_best_effort; -use openshell_core::{ComputeDriverKind, Config, Error, ObjectLabels, Result}; +use openshell_core::{Config, Error, ObjectLabels, Result}; use openshell_extension_core::{ BearerTokenSlot, ExtensionAudience, ExtensionCallerKind, ExtensionKind, MAX_EXTENSION_TOKEN_TTL, }; @@ -67,6 +59,7 @@ use openshell_supervisor_middleware::MiddlewareRegistry; use std::collections::{BTreeMap, HashMap}; use std::io::ErrorKind; use std::net::SocketAddr; +use std::path::Path; #[cfg(test)] use std::sync::LazyLock; use std::sync::{Arc, Mutex}; @@ -438,6 +431,7 @@ impl ServerState { pub(crate) async fn run_server( startup: ServerStartupConfig, tracing_log_bus: TracingLogBus, + compute_drivers: ComputeDriverRegistry, ) -> Result<()> { let ServerStartupConfig { config, @@ -591,6 +585,7 @@ pub(crate) async fn run_server( endpoint_overrides: &config.compute_driver_endpoints, }; let (compute, operator_allowlist) = build_compute_runtime( + &compute_drivers, &config, driver_startup, store.clone(), @@ -1079,13 +1074,405 @@ fn unsupported_builtin_compute_driver(driver: ComputeDriverKind) -> compute::Com )) } -// Internal wiring helper: each argument is a distinct piece of runtime state -// that must be passed through, so the count is justified. -#[allow(clippy::too_many_arguments)] type OperatorAllowlistArc = Option; +pub use compute::{DriverWatchStream, SharedComputeDriver}; + +/// Opaque result returned by a compiled compute-driver factory. +pub struct ComputeDriverBuildOutput { + runtime: ComputeRuntime, + operator_allowlist: OperatorAllowlistArc, +} + +/// Factory for a compute driver linked into a gateway binary. +#[async_trait::async_trait] +pub trait ComputeDriverFactory: Send + Sync { + async fn build( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result; +} + +/// One named compiled-driver registration. +#[derive(Clone)] +pub struct ComputeDriverRegistration { + name: String, + detection_priority: u16, + detect: Option bool>, + factory: Arc, +} + +impl std::fmt::Debug for ComputeDriverRegistration { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ComputeDriverRegistration") + .field("name", &self.name) + .field("detection_priority", &self.detection_priority) + .field("has_detection_probe", &self.detect.is_some()) + .finish_non_exhaustive() + } +} + +impl ComputeDriverRegistration { + /// Define a compiled driver. Lower detection priorities are preferred. + pub fn new( + name: impl Into, + detection_priority: u16, + detect: Option bool>, + factory: impl ComputeDriverFactory + 'static, + ) -> Result { + let name = openshell_core::config::normalize_compute_driver_name(&name.into()) + .map_err(Error::config)?; + Ok(Self { + name, + detection_priority, + detect, + factory: Arc::new(factory), + }) + } +} + +/// Registry of compute drivers compiled into this gateway binary. +/// +/// Like `SQLx`'s `Any` driver registry, installation is explicit at the binary +/// composition boundary while runtime selection is generic. +#[derive(Clone, Default)] +pub struct ComputeDriverRegistry { + drivers: BTreeMap, +} + +impl ComputeDriverRegistry { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Install a compiled driver factory. + pub fn install(&mut self, registration: ComputeDriverRegistration) -> Result<()> { + let name = registration.name.clone(); + match self.drivers.entry(name.clone()) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(registration); + Ok(()) + } + std::collections::btree_map::Entry::Occupied(_) => Err(Error::config(format!( + "compute driver '{name}' registered twice" + ))), + } + } + + /// Names installed into this gateway binary, in lexical order. + pub fn installed_driver_names(&self) -> impl Iterator { + self.drivers.keys().map(String::as_str) + } + + fn get(&self, name: &str) -> Option<&ComputeDriverRegistration> { + self.drivers.get(name) + } + + fn detect(&self) -> Option<&ComputeDriverRegistration> { + self.drivers + .values() + .filter(|registration| registration.detect.is_some_and(|detect| detect())) + .min_by_key(|registration| registration.detection_priority) + } +} + +/// Install every first-party compute driver linked into the standard gateway. +#[must_use] +pub fn install_default_compute_drivers() -> ComputeDriverRegistry { + let mut registry = ComputeDriverRegistry::new(); + #[cfg(not(target_os = "windows"))] + { + registry + .install( + ComputeDriverRegistration::new( + "kubernetes", + 100, + Some(|| std::env::var_os("KUBERNETES_SERVICE_HOST").is_some()), + KubernetesComputeDriverFactory, + ) + .expect("valid kubernetes registration"), + ) + .expect("unique kubernetes registration"); + registry + .install( + ComputeDriverRegistration::new( + "podman", + 200, + Some(openshell_core::config::is_podman_available), + PodmanComputeDriverFactory, + ) + .expect("valid podman registration"), + ) + .expect("unique podman registration"); + registry + .install( + ComputeDriverRegistration::new( + "docker", + 300, + Some(openshell_core::config::is_docker_available), + DockerComputeDriverFactory, + ) + .expect("valid docker registration"), + ) + .expect("unique docker registration"); + registry + .install( + ComputeDriverRegistration::new("vm", u16::MAX, None, VmComputeDriverFactory) + .expect("valid vm registration"), + ) + .expect("unique vm registration"); + } + #[cfg(target_os = "windows")] + for name in ["kubernetes", "podman", "docker", "vm"] { + registry + .install( + ComputeDriverRegistration::new( + name, + u16::MAX, + None, + UnsupportedComputeDriverFactory, + ) + .expect("valid unsupported registration"), + ) + .expect("unique unsupported registration"); + } + registry +} + +pub struct ComputeDriverBuildContext<'a> { + driver_name: String, + config: &'a Config, + driver_startup: compute::driver_config::DriverStartupContext<'a>, + store: Arc, + sandbox_index: SandboxIndex, + sandbox_watch_bus: SandboxWatchBus, + tracing_log_bus: TracingLogBus, + supervisor_sessions: Arc, + shutdown_rx: watch::Receiver, +} + +impl ComputeDriverBuildContext<'_> { + #[must_use] + pub fn driver_name(&self) -> &str { + &self.driver_name + } + + #[must_use] + pub fn gateway_config(&self) -> &Config { + self.config + } + + #[must_use] + pub fn gateway_port(&self) -> u16 { + self.driver_startup.gateway_port + } + + #[must_use] + pub fn gateway_tls_enabled(&self) -> bool { + self.driver_startup.gateway_tls_enabled + } + + /// Gateway client credentials that a local driver may mount into guests. + #[must_use] + pub fn guest_tls_paths(&self) -> Option<(&Path, &Path, &Path)> { + self.driver_startup + .guest_tls + .map(compute::driver_config::GuestTlsPaths::as_paths) + } + + /// Deserialize the selected driver's merged TOML table. + pub fn driver_config(&self) -> Result + where + T: Default + serde::de::DeserializeOwned, + { + compute::driver_config::driver_config_from_context(self.driver_startup, &self.driver_name) + } + + #[must_use] + pub fn shutdown_receiver(&self) -> watch::Receiver { + self.shutdown_rx.clone() + } + + /// Finish construction of an in-process driver through the common runtime path. + pub async fn finish_in_process( + self, + driver: SharedComputeDriver, + ) -> Result { + let runtime = ComputeRuntime::from_driver( + self.driver_name, + driver, + None, + self.store, + self.sandbox_index, + self.sandbox_watch_bus, + self.tracing_log_bus, + self.supervisor_sessions, + ) + .await + .map_err(|error| Error::execution(format!("failed to create compute runtime: {error}")))?; + Ok(ComputeDriverBuildOutput { + runtime, + operator_allowlist: None, + }) + } +} + +#[cfg(target_os = "windows")] +#[derive(Clone, Copy)] +struct UnsupportedComputeDriverFactory; + +#[cfg(target_os = "windows")] +#[async_trait::async_trait] +impl ComputeDriverFactory for UnsupportedComputeDriverFactory { + async fn build( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result { + Err(Error::execution( + unsupported_builtin_compute_driver( + context + .driver_name + .parse() + .expect("default driver names are valid"), + ) + .to_string(), + )) + } +} + +#[cfg(not(target_os = "windows"))] +#[derive(Clone, Copy)] +struct KubernetesComputeDriverFactory; + +#[cfg(not(target_os = "windows"))] +#[async_trait::async_trait] +impl ComputeDriverFactory for KubernetesComputeDriverFactory { + async fn build( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result { + warn_if_kubernetes_sandbox_jwt_expiry_disabled(context.config); + let config = compute::driver_config::builtin::kubernetes_config_from_context( + context.driver_startup, + )?; + let (runtime, operator_allowlist) = ComputeRuntime::new_kubernetes( + config, + context.store, + context.sandbox_index, + context.sandbox_watch_bus, + context.tracing_log_bus, + context.supervisor_sessions, + context.shutdown_rx, + ) + .await + .map_err(|error| Error::execution(format!("failed to create compute runtime: {error}")))?; + Ok(ComputeDriverBuildOutput { + runtime, + operator_allowlist, + }) + } +} + +#[cfg(not(target_os = "windows"))] +#[derive(Clone, Copy)] +struct DockerComputeDriverFactory; + +#[cfg(not(target_os = "windows"))] +#[async_trait::async_trait] +impl ComputeDriverFactory for DockerComputeDriverFactory { + async fn build( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result { + let driver_config = + compute::driver_config::builtin::docker_config_from_context(context.driver_startup)?; + let runtime = ComputeRuntime::new_docker( + context.config.clone(), + driver_config, + context.store, + context.sandbox_index, + context.sandbox_watch_bus, + context.tracing_log_bus, + context.supervisor_sessions, + ) + .await + .map_err(|error| Error::execution(format!("failed to create compute runtime: {error}")))?; + Ok(ComputeDriverBuildOutput { + runtime, + operator_allowlist: None, + }) + } +} + +#[cfg(not(target_os = "windows"))] +#[derive(Clone, Copy)] +struct PodmanComputeDriverFactory; + +#[cfg(not(target_os = "windows"))] +#[async_trait::async_trait] +impl ComputeDriverFactory for PodmanComputeDriverFactory { + async fn build( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result { + let driver_config = + compute::driver_config::builtin::podman_config_from_context(context.driver_startup)?; + let runtime = ComputeRuntime::new_podman( + driver_config, + context.store, + context.sandbox_index, + context.sandbox_watch_bus, + context.tracing_log_bus, + context.supervisor_sessions, + ) + .await + .map_err(|error| Error::execution(format!("failed to create compute runtime: {error}")))?; + Ok(ComputeDriverBuildOutput { + runtime, + operator_allowlist: None, + }) + } +} + +#[cfg(not(target_os = "windows"))] +#[derive(Clone, Copy)] +struct VmComputeDriverFactory; + +#[cfg(not(target_os = "windows"))] +#[async_trait::async_trait] +impl ComputeDriverFactory for VmComputeDriverFactory { + async fn build( + &self, + context: ComputeDriverBuildContext<'_>, + ) -> Result { + let driver_config = + compute::driver_config::builtin::vm_config_from_context(context.driver_startup)?; + let otlp_config = context + .driver_startup + .file + .and_then(|file| file.openshell.gateway.otlp.as_ref()); + let endpoint = compute::vm::spawn(context.config, &driver_config, otlp_config).await?; + let runtime = ComputeRuntime::new_remote_driver( + endpoint, + context.store, + context.sandbox_index, + context.sandbox_watch_bus, + context.tracing_log_bus, + context.supervisor_sessions, + ) + .await + .map_err(|error| Error::execution(format!("failed to create compute runtime: {error}")))?; + Ok(ComputeDriverBuildOutput { + runtime, + operator_allowlist: None, + }) + } +} #[allow(clippy::too_many_arguments)] async fn build_compute_runtime( + registry: &ComputeDriverRegistry, config: &Config, driver_startup: compute::driver_config::DriverStartupContext<'_>, store: Arc, @@ -1095,86 +1482,26 @@ async fn build_compute_runtime( supervisor_sessions: Arc, shutdown_rx: watch::Receiver, ) -> Result<(ComputeRuntime, OperatorAllowlistArc)> { - let driver = configured_compute_driver(config, driver_startup)?; + let driver = configured_compute_driver(registry, config, driver_startup)?; info!(driver = %driver.name(), "Using compute driver"); let (runtime, operator_allowlist) = match driver { - #[cfg(target_os = "windows")] - ConfiguredComputeDriver::Builtin(driver) => { - return Err(Error::execution( - unsupported_builtin_compute_driver(driver).to_string(), - )); - } - #[cfg(not(target_os = "windows"))] - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Kubernetes) => { - warn_if_kubernetes_sandbox_jwt_expiry_disabled(config); - let k8s_config = - compute::driver_config::builtin::kubernetes_config_from_context(driver_startup)?; - let (rt, allowlist) = ComputeRuntime::new_kubernetes( - k8s_config, - store, - sandbox_index, - sandbox_watch_bus, - tracing_log_bus, - supervisor_sessions.clone(), - shutdown_rx, - ) - .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))?; - (rt, allowlist) - } - #[cfg(not(target_os = "windows"))] - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Docker) => { - let docker_config = - compute::driver_config::builtin::docker_config_from_context(driver_startup)?; - let rt = ComputeRuntime::new_docker( - config.clone(), - docker_config, - store, - sandbox_index, - sandbox_watch_bus, - tracing_log_bus, - supervisor_sessions, - ) - .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))?; - (rt, None) - } - #[cfg(not(target_os = "windows"))] - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Podman) => { - let podman_config = - compute::driver_config::builtin::podman_config_from_context(driver_startup)?; - let rt = ComputeRuntime::new_podman( - podman_config, - store, - sandbox_index, - sandbox_watch_bus, - tracing_log_bus, - supervisor_sessions, - ) - .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))?; - (rt, None) - } - #[cfg(not(target_os = "windows"))] - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Vm) => { - let vm_config = - compute::driver_config::builtin::vm_config_from_context(driver_startup)?; - let otlp_config = driver_startup - .file - .and_then(|file| file.openshell.gateway.otlp.as_ref()); - let endpoint = compute::vm::spawn(config, &vm_config, otlp_config).await?; - let rt = ComputeRuntime::new_remote_driver( - endpoint, - store, - sandbox_index, - sandbox_watch_bus, - tracing_log_bus, - supervisor_sessions, - ) - .await - .map_err(|e| Error::execution(format!("failed to create compute runtime: {e}")))?; - (rt, None) + ConfiguredComputeDriver::Registered(registration) => { + let output = registration + .factory + .build(ComputeDriverBuildContext { + driver_name: registration.name, + config, + driver_startup, + store, + sandbox_index, + sandbox_watch_bus, + tracing_log_bus, + supervisor_sessions, + shutdown_rx, + }) + .await?; + (output.runtime, output.operator_allowlist) } ConfiguredComputeDriver::Remote { name } => { let remote_config = @@ -1206,35 +1533,35 @@ async fn build_compute_runtime( #[derive(Debug, Clone)] enum ConfiguredComputeDriver { - Builtin(ComputeDriverKind), + Registered(ComputeDriverRegistration), Remote { name: String }, } impl ConfiguredComputeDriver { fn name(&self) -> &str { match self { - Self::Builtin(kind) => kind.as_str(), + Self::Registered(registration) => ®istration.name, Self::Remote { name } => name, } } } fn configured_compute_driver( + registry: &ComputeDriverRegistry, config: &Config, driver_startup: compute::driver_config::DriverStartupContext<'_>, ) -> Result { match config.compute_drivers.as_slice() { - [] => match openshell_core::config::detect_driver() { - Some(ComputeDriverKind::Vm) => Err(Error::config( - "vm compute driver is opt-in only; set --drivers vm or OPENSHELL_DRIVERS=vm", - )), - Some(driver) => Ok(ConfiguredComputeDriver::Builtin(driver)), - 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", - )), - }, - [driver] => resolve_configured_compute_driver(driver, driver_startup), + [] => registry.detect().map_or_else( + || { + 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", + )) + }, + |registration| Ok(ConfiguredComputeDriver::Registered(registration.clone())), + ), + [driver] => resolve_configured_compute_driver(registry, driver, driver_startup), drivers => Err(Error::config(format!( "multiple compute drivers are not supported yet; configured drivers: {}", drivers.join(",") @@ -1243,30 +1570,26 @@ fn configured_compute_driver( } fn resolve_configured_compute_driver( + registry: &ComputeDriverRegistry, driver_name: &str, driver_startup: compute::driver_config::DriverStartupContext<'_>, ) -> Result { let name = openshell_core::config::normalize_compute_driver_name(driver_name) .map_err(Error::config)?; // An operator-provided endpoint replaces normal construction for the - // selected name. The gateway connects to it; it does not provision a - // remote implementation for canonical built-in names. + // selected name, including a compiled registration with the same name. + // The gateway connects to it; it does not provision the remote driver. if driver_startup.endpoint_overrides.contains_key(&name) { return Ok(ConfiguredComputeDriver::Remote { name }); } - let driver_kind = builtin_compute_driver(&name); - if let Some(kind) = driver_kind { - return Ok(ConfiguredComputeDriver::Builtin(kind)); + if let Some(registration) = registry.get(&name) { + return Ok(ConfiguredComputeDriver::Registered(registration.clone())); } Ok(ConfiguredComputeDriver::Remote { name }) } -fn builtin_compute_driver(name: &str) -> Option { - name.parse().ok() -} - fn kubernetes_sandbox_jwt_expiry_disabled(config: &Config) -> bool { config .gateway_jwt @@ -1474,6 +1797,23 @@ mod tests { } } + fn test_compute_drivers() -> super::ComputeDriverRegistry { + super::install_default_compute_drivers() + } + + #[derive(Clone, Copy)] + struct TestComputeDriverFactory; + + #[async_trait::async_trait] + impl super::ComputeDriverFactory for TestComputeDriverFactory { + async fn build( + &self, + _context: super::ComputeDriverBuildContext<'_>, + ) -> openshell_core::Result { + unreachable!("selection tests do not construct the driver") + } + } + fn test_tls_acceptor() -> (TempDir, TlsAcceptor) { install_rustls_provider(); @@ -1787,18 +2127,21 @@ 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, test_driver_startup(&config, None)); + let result = configured_compute_driver( + &test_compute_drivers(), + &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)) => { + Ok(ConfiguredComputeDriver::Registered(registration)) => { assert!( matches!( - driver, - ComputeDriverKind::Kubernetes - | ComputeDriverKind::Docker - | ComputeDriverKind::Podman + registration.name.as_str(), + "kubernetes" | "docker" | "podman" ), - "auto-detected unexpected driver: {driver:?}" + "auto-detected unexpected driver: {}", + registration.name ); } Ok(ConfiguredComputeDriver::Remote { name }) => { @@ -1814,12 +2157,58 @@ mod tests { } } + #[test] + fn registry_detection_uses_registered_priorities() { + fn available() -> bool { + true + } + + let mut registry = super::ComputeDriverRegistry::new(); + registry + .install( + super::ComputeDriverRegistration::new( + "later", + 200, + Some(available), + TestComputeDriverFactory, + ) + .unwrap(), + ) + .unwrap(); + registry + .install( + super::ComputeDriverRegistration::new( + "earlier", + 100, + Some(available), + TestComputeDriverFactory, + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!( + registry + .detect() + .map(|registration| registration.name.as_str()), + Some("earlier") + ); + assert_eq!( + registry.installed_driver_names().collect::>(), + vec!["earlier", "later"] + ); + } + #[test] 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, test_driver_startup(&config, None)).unwrap_err(); + let err = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap_err(); assert!( err.to_string() .contains("multiple compute drivers are not supported yet") @@ -1830,33 +2219,45 @@ 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, test_driver_startup(&config, None)).unwrap(); + let driver = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap(); assert!(matches!( driver, - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Podman) + ConfiguredComputeDriver::Registered(registration) if registration.name == "podman" )); } #[test] fn configured_compute_driver_accepts_vm() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Vm]); - let driver = - configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); + let driver = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap(); assert!(matches!( driver, - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Vm) + ConfiguredComputeDriver::Registered(registration) if registration.name == "vm" )); } #[test] fn configured_compute_driver_accepts_docker() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Docker]); - let driver = - configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); + let driver = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap(); assert!(matches!( driver, - ConfiguredComputeDriver::Builtin(ComputeDriverKind::Docker) + ConfiguredComputeDriver::Registered(registration) if registration.name == "docker" )); } @@ -1864,15 +2265,22 @@ mod tests { fn configured_compute_driver_resolves_named_remote() { let config = Config::new(None).with_compute_drivers(["kyma"]); - let driver = - configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); + let driver = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap(); match driver { ConfiguredComputeDriver::Remote { name } => { assert_eq!(name, "kyma"); } - ConfiguredComputeDriver::Builtin(other) => { - panic!("expected remote driver, got builtin driver {other:?}") + ConfiguredComputeDriver::Registered(other) => { + panic!( + "expected remote driver, got registered driver {}", + other.name + ) } } } @@ -1883,8 +2291,12 @@ mod tests { .with_compute_drivers([ComputeDriverKind::Vm]) .with_compute_driver_endpoint("vm", "/run/openshell/vm.sock"); - let driver = - configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); + let driver = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap(); assert!(matches!( driver, ConfiguredComputeDriver::Remote { name } if name == "vm" @@ -1897,8 +2309,12 @@ mod tests { .with_compute_drivers([ComputeDriverKind::Docker]) .with_compute_driver_endpoint("docker", "/run/openshell/docker.sock"); - let driver = - configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap(); + let driver = configured_compute_driver( + &test_compute_drivers(), + &config, + test_driver_startup(&config, None), + ) + .unwrap(); assert!(matches!( driver, ConfiguredComputeDriver::Remote { name } if name == "docker" diff --git a/crates/openshell-server/src/main.rs b/crates/openshell-server/src/main.rs index 0f33c685f4..c76761016d 100644 --- a/crates/openshell-server/src/main.rs +++ b/crates/openshell-server/src/main.rs @@ -7,5 +7,8 @@ use miette::Result; #[tokio::main] async fn main() -> Result<()> { - openshell_server::cli::run_cli().await + openshell_server::cli::run_cli_with_compute_drivers( + openshell_server::install_default_compute_drivers(), + ) + .await }