diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 7713febb6f..2515e7bb67 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -125,6 +125,9 @@ jobs: - name: Verify telemetry can be compiled out run: mise run rust:verify:telemetry-off + - name: Verify each compute driver can be compiled out + run: mise run rust:verify:drivers-off + - name: sccache stats if: always() run: | diff --git a/README.md b/README.md index 2b9749652a..74905e4e2f 100644 --- a/README.md +++ b/README.md @@ -252,12 +252,23 @@ OpenShell collects anonymous telemetry to help improve the project for developer Disable telemetry at runtime by setting `OPENSHELL_TELEMETRY_ENABLED=false` on the gateway deployment. OpenShell propagates this deployment setting into sandbox supervisor environments so sandbox-side telemetry collection is disabled as well. -You can also compile telemetry out entirely. Telemetry support is a default-on `telemetry` Cargo feature; building with `--no-default-features` produces binaries that contain no telemetry endpoint, no telemetry HTTP client, and no emission code. Build telemetry-free artifacts with, for example, `cargo build --release -p openshell-server --no-default-features` (gateway) and the equivalent for `openshell-sandbox` and `openshell-driver-vm`. With telemetry compiled out, the gateway emits nothing and reports telemetry disabled to the sandboxes it launches. +You can also compile telemetry out entirely. Telemetry support is a default-on `telemetry` Cargo feature; building `openshell-server` with `--no-default-features` and selecting only the driver features you need produces a gateway that contains no telemetry endpoint, no telemetry HTTP client, and no emission code. The gateway requires at least one compute driver to link; for example, `cargo build --release -p openshell-server --bin openshell-gateway --no-default-features --features driver-kubernetes,driver-docker,driver-podman,driver-vm` builds a telemetry-free gateway with every driver. Build telemetry-free `openshell-sandbox` and `openshell-driver-vm` binaries with a plain `--no-default-features`. With telemetry compiled out, the gateway emits nothing and reports telemetry disabled to the sandboxes it launches. Telemetry events are limited to anonymous operational categories and counts, such as sandbox lifecycle outcomes, provider profile buckets, policy decision counts, and aggregate network activity denial categories. OpenShell telemetry does not collect sandbox names or IDs, hostnames, file paths, binary paths, prompts, credentials, provider names, model names, or user content. Opting out applies only to telemetry emitted by OpenShell. Third-party services, model providers, inference endpoints, agents, or tools that you configure and use with OpenShell may have their own terms and privacy practices. +## Choosing Which Compute Drivers Are Compiled In + +Each built-in compute driver — Kubernetes, Docker, Podman, VM — is gated by a Cargo feature on `openshell-server` (`driver-kubernetes`, `driver-docker`, `driver-podman`, `driver-vm`), all enabled by default. Operators who only need one driver can produce a slimmer gateway with a reduced supply-chain surface by building with `--no-default-features` and enabling the drivers they want: + +```shell +cargo build --release -p openshell-server --bin openshell-gateway \ + --no-default-features --features driver-kubernetes,telemetry +``` + +Any non-empty driver subset is a valid build, including zero built-in drivers (an extension-only gateway that reaches an out-of-tree driver over `[openshell.drivers.].socket_path`). What the gateway needs at *runtime* is a `compute_drivers` entry it can serve: either a built-in this binary was built with, or an extension driver name with a matching socket_path entry. Naming a built-in that this gateway was not built with fails at startup with a message naming both the driver and the Cargo flag that re-enables it. Extension drivers reached via `--compute-driver-socket` work regardless of the feature set. See the [gateway configuration reference](docs/reference/gateway-config.mdx) for details. + ## Notice and Disclaimer This software automatically retrieves, accesses or interacts with external materials. Those retrieved materials are not distributed with this software and are governed solely by separate terms, conditions and licenses. You are solely responsible for finding, reviewing and complying with all applicable terms, conditions, and licenses, and for verifying the security, integrity and suitability of any retrieved materials for your specific use case. This software is provided "AS IS", without warranty of any kind. The author makes no representations or warranties regarding any retrieved materials, and assumes no liability for any losses, damages, liabilities or legal consequences from your use or inability to use this software or any retrieved materials. Use this software and the retrieved materials at your own risk. diff --git a/architecture/compute-runtimes.md b/architecture/compute-runtimes.md index f122fda5d7..06eb2633a0 100644 --- a/architecture/compute-runtimes.md +++ b/architecture/compute-runtimes.md @@ -117,3 +117,27 @@ them without changing the sandbox architecture. When runtime infrastructure changes, validate the relevant sandbox e2e path and update the matching driver README if a maintainer-facing constraint changes. + +## Compile-Time Driver Selection + +Each built-in runtime is gated by a Cargo feature on `openshell-server`: +`driver-kubernetes`, `driver-docker`, `driver-podman`, `driver-vm`. All +four are on by default, so the default gateway is byte-equivalent to +before the feature split. Building `--no-default-features` and selecting a +subset produces a gateway that carries only the requested drivers, along +with their transitive dependencies and driver-specific plumbing (Docker's +supervisor readiness impl, the K8s ServiceAccount authenticator, the VM +subprocess launcher, and so on). + +A zero-driver build is a supported extension-only gateway shape. It +carries no in-tree driver code and speaks `compute_driver.proto` only to +out-of-tree drivers named in `compute_drivers` with a matching +`[openshell.drivers.].socket_path`. A driver named in +`gateway.toml` that this binary was not built with fails at startup with +a message naming both the driver and the Cargo flag that re-enables it; +auto-detection skips drivers that were compiled out and falls through to +a "no suitable driver" error rather than silently picking a +different backend. Extension drivers reached via +`--compute-driver-socket` are always available regardless of driver +features — they run out-of-process and the gateway only needs to speak +`compute_driver.proto`. diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index ba6b9d401f..8cc540e0a3 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -49,7 +49,7 @@ pub const CDI_GPU_DEVICE_ALL: &str = "nvidia.com/gpu=all"; pub const DEFAULT_SANDBOX_PIDS_LIMIT: i64 = 2048; /// Compute backends the gateway can orchestrate sandboxes through. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ComputeDriverKind { Kubernetes, diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index d9bea99e7c..da64ec8740 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -5,6 +5,15 @@ #![allow(clippy::result_large_err)] +/// Compile-out guard: greppable proof this crate is linked into a binary. +/// +/// `tasks/scripts/verify-drivers-compiled-out.sh` asserts this string is +/// present when `--features driver-docker` is on and absent when it is +/// off. `#[used]` prevents dead-code elimination; the linker cannot drop it. +/// Do not remove without updating the verify script. +#[used] +static COMPILE_MARKER: &str = "OPENSHELL_DRIVER_MARKER:docker"; + use bollard::Docker; use bollard::errors::Error as BollardError; use bollard::models::{ diff --git a/crates/openshell-driver-kubernetes/src/lib.rs b/crates/openshell-driver-kubernetes/src/lib.rs index 953ed4abdf..79595d8d0d 100644 --- a/crates/openshell-driver-kubernetes/src/lib.rs +++ b/crates/openshell-driver-kubernetes/src/lib.rs @@ -11,3 +11,12 @@ pub use config::{ }; pub use driver::{KubernetesComputeDriver, KubernetesDriverError}; pub use grpc::ComputeDriverService; + +/// Compile-out guard: greppable proof this crate is linked into a binary. +/// +/// `tasks/scripts/verify-drivers-compiled-out.sh` asserts this string is +/// present when `--features driver-kubernetes` is on and absent when it is +/// off. `#[used]` prevents dead-code elimination; the linker cannot drop it. +/// Do not remove without updating the verify script. +#[used] +static COMPILE_MARKER: &str = "OPENSHELL_DRIVER_MARKER:kubernetes"; diff --git a/crates/openshell-driver-podman/src/lib.rs b/crates/openshell-driver-podman/src/lib.rs index 5847a10ea6..f25fee53ab 100644 --- a/crates/openshell-driver-podman/src/lib.rs +++ b/crates/openshell-driver-podman/src/lib.rs @@ -13,3 +13,12 @@ pub(crate) mod watcher; pub use config::PodmanComputeConfig; pub use driver::PodmanComputeDriver; pub use grpc::ComputeDriverService; + +/// Compile-out guard: greppable proof this crate is linked into a binary. +/// +/// `tasks/scripts/verify-drivers-compiled-out.sh` asserts this string is +/// present when `--features driver-podman` is on and absent when it is +/// off. `#[used]` prevents dead-code elimination; the linker cannot drop it. +/// Do not remove without updating the verify script. +#[used] +static COMPILE_MARKER: &str = "OPENSHELL_DRIVER_MARKER:podman"; diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index b5c9b34d71..24dd16bb4e 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -17,9 +17,10 @@ path = "src/main.rs" [dependencies] openshell-bootstrap = { path = "../openshell-bootstrap" } openshell-core = { path = "../openshell-core", default-features = false } -openshell-driver-docker = { path = "../openshell-driver-docker" } -openshell-driver-kubernetes = { path = "../openshell-driver-kubernetes" } -openshell-driver-podman = { path = "../openshell-driver-podman" } +# Compute driver crates are gated by the `driver-*` features below. +openshell-driver-docker = { path = "../openshell-driver-docker", optional = true } +openshell-driver-kubernetes = { path = "../openshell-driver-kubernetes", optional = true } +openshell-driver-podman = { path = "../openshell-driver-podman", optional = true } openshell-ocsf = { path = "../openshell-ocsf" } openshell-policy = { path = "../openshell-policy" } openshell-prover = { path = "../openshell-prover" } @@ -99,11 +100,24 @@ arc-swap = "1" notify = "8" [features] -default = ["telemetry"] +default = [ + "telemetry", + "driver-kubernetes", + "driver-docker", + "driver-podman", + "driver-vm", +] ## Compile in anonymous telemetry emission (forwards to openshell-core/telemetry). ## On by default; build with `--no-default-features` for a telemetry-free gateway ## that contains no telemetry endpoint, HTTP client, or emission code. telemetry = ["openshell-core/telemetry"] +## Compile in the in-tree compute drivers. +driver-kubernetes = ["dep:openshell-driver-kubernetes"] +driver-docker = ["dep:openshell-driver-docker"] +driver-podman = ["dep:openshell-driver-podman"] +## The VM driver itself runs out-of-process; +## this feature gates the in-server code that spawns and speaks gRPC to it. +driver-vm = [] bundled-z3 = ["openshell-prover/bundled-z3"] test-support = [] diff --git a/crates/openshell-server/src/auth/mod.rs b/crates/openshell-server/src/auth/mod.rs index cbf3b94d91..08a2fb199e 100644 --- a/crates/openshell-server/src/auth/mod.rs +++ b/crates/openshell-server/src/auth/mod.rs @@ -13,6 +13,7 @@ pub mod authz; pub mod guard; mod http; pub mod identity; +#[cfg(feature = "driver-kubernetes")] pub mod k8s_sa; pub mod method_authz; pub mod oidc; diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 9aee2bc6d6..9b87a1df40 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -14,6 +14,11 @@ use tracing::{info, warn}; use tracing_subscriber::EnvFilter; use crate::certgen; +#[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" +))] use crate::compute::driver_config::GuestTlsPaths; use crate::config_file::{self, ConfigFile, GatewayFileSection}; use crate::defaults::{self, LocalTlsPaths}; @@ -247,8 +252,13 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result Result Result> { Ok(default_path.is_file().then_some(default_path)) } -fn apply_runtime_defaults(args: &mut RunArgs) -> Result> { - let local_tls = if args.disable_tls { - None - } else { - defaults::complete_local_tls_paths()? - }; +fn apply_runtime_defaults(args: &mut RunArgs) -> Result<()> { + let local_tls = resolved_local_tls(args)?; if args.db_url.is_none() { args.db_url = Some(defaults::default_database_url()?); @@ -516,7 +527,17 @@ fn apply_runtime_defaults(args: &mut RunArgs) -> Result> { args.tls_client_ca = Some(paths.ca.clone()); } - Ok(local_tls) + Ok(()) +} + +/// Read the local TLS bundle if TLS is not disabled and a complete bundle +/// exists on disk. +fn resolved_local_tls(args: &RunArgs) -> Result> { + if args.disable_tls { + Ok(None) + } else { + defaults::complete_local_tls_paths() + } } /// Returns `true` when an argument's value came from clap's built-in default @@ -1156,7 +1177,8 @@ mod tests { let _g2 = EnvVarGuard::set("XDG_STATE_HOME", tmp.path().to_str().unwrap()); let (mut args, _) = parse_with_args(&["openshell-gateway", "--disable-tls"]); - let local_tls = super::apply_runtime_defaults(&mut args).unwrap(); + super::apply_runtime_defaults(&mut args).unwrap(); + let local_tls = super::resolved_local_tls(&args).unwrap(); let expected = format!( "sqlite:{}", @@ -1195,7 +1217,8 @@ mod tests { } let (mut args, _) = parse_with_args(&["openshell-gateway"]); - let local_tls = super::apply_runtime_defaults(&mut args) + super::apply_runtime_defaults(&mut args).unwrap(); + let local_tls = super::resolved_local_tls(&args) .unwrap() .expect("complete bundle should be returned"); @@ -1764,6 +1787,7 @@ mem_mib = "not-a-number" } #[test] + #[cfg(feature = "driver-kubernetes")] fn driver_inherits_shared_image_from_gateway_section() { // [openshell.gateway].default_image inherits into the K8s driver // table when the driver-specific table does not set it. @@ -1789,6 +1813,7 @@ namespace = "agents" } #[test] + #[cfg(feature = "driver-kubernetes")] fn driver_specific_value_overrides_gateway_inheritance() { let file = config_file_from_toml( r#" diff --git a/crates/openshell-server/src/compute/driver_config.rs b/crates/openshell-server/src/compute/driver_config.rs index 294cec0ce7..66c5433d63 100644 --- a/crates/openshell-server/src/compute/driver_config.rs +++ b/crates/openshell-server/src/compute/driver_config.rs @@ -8,17 +8,38 @@ //! It does not acquire, connect to, or start compute drivers. use crate::config_file; +#[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" +))] use crate::defaults::LocalTlsPaths; -use openshell_core::{ComputeDriverKind, Error, Result}; +#[cfg(any( + feature = "driver-kubernetes", + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm", +))] +use openshell_core::ComputeDriverKind; +use openshell_core::{Error, Result}; +#[cfg(feature = "driver-docker")] use openshell_driver_docker::DockerComputeConfig; +#[cfg(feature = "driver-kubernetes")] use openshell_driver_kubernetes::KubernetesComputeConfig; +#[cfg(feature = "driver-podman")] use openshell_driver_podman::PodmanComputeConfig; use serde::Deserialize; use std::collections::BTreeMap; use std::path::PathBuf; +#[cfg(feature = "driver-vm")] use super::VmComputeConfig; +#[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" +))] #[derive(Debug, Clone, PartialEq, Eq)] pub struct GuestTlsPaths { ca: PathBuf, @@ -26,6 +47,11 @@ pub struct GuestTlsPaths { key: PathBuf, } +#[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" +))] impl From<&LocalTlsPaths> for GuestTlsPaths { fn from(paths: &LocalTlsPaths) -> Self { Self { @@ -39,13 +65,22 @@ impl From<&LocalTlsPaths> for GuestTlsPaths { #[derive(Clone, Copy)] pub struct DriverStartupContext<'a> { pub file: Option<&'a config_file::ConfigFile>, + // Consumed only by the docker/podman/vm runtime-defaults helpers below. + #[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" + ))] pub guest_tls: Option<&'a GuestTlsPaths>, + #[cfg(any(feature = "driver-podman", feature = "driver-vm"))] pub gateway_port: u16, + #[cfg(feature = "driver-vm")] pub gateway_tls_enabled: bool, pub endpoint_overrides: &'a BTreeMap, } /// Build the selected Kubernetes config from TOML plus runtime defaults. +#[cfg(feature = "driver-kubernetes")] pub fn kubernetes_config_from_context( context: DriverStartupContext<'_>, ) -> Result { @@ -54,6 +89,7 @@ pub fn kubernetes_config_from_context( Ok(cfg) } +#[cfg(feature = "driver-kubernetes")] pub fn kubernetes_config_for_k8s_sa_bootstrap( file: Option<&config_file::ConfigFile>, ) -> Result { @@ -71,6 +107,7 @@ pub fn kubernetes_config_for_k8s_sa_bootstrap( } /// Build the selected Podman config from TOML plus runtime defaults. +#[cfg(feature = "driver-podman")] pub fn podman_config_from_context( context: DriverStartupContext<'_>, ) -> Result { @@ -80,6 +117,7 @@ pub fn podman_config_from_context( } /// Build the selected Docker config from TOML plus runtime defaults. +#[cfg(feature = "driver-docker")] pub fn docker_config_from_context( context: DriverStartupContext<'_>, ) -> Result { @@ -89,6 +127,7 @@ pub fn docker_config_from_context( } /// Build the selected VM config from TOML plus runtime defaults. +#[cfg(feature = "driver-vm")] 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); @@ -141,12 +180,14 @@ where }) } +#[cfg(feature = "driver-kubernetes")] 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; } } +#[cfg(feature = "driver-podman")] fn apply_podman_runtime_defaults( podman: &mut PodmanComputeConfig, context: DriverStartupContext<'_>, @@ -161,6 +202,7 @@ fn apply_podman_runtime_defaults( ); } +#[cfg(feature = "driver-docker")] fn apply_docker_runtime_defaults(cfg: &mut DockerComputeConfig, context: DriverStartupContext<'_>) { apply_guest_tls_defaults_to_split_fields( &mut cfg.guest_tls_ca, @@ -170,6 +212,7 @@ fn apply_docker_runtime_defaults(cfg: &mut DockerComputeConfig, context: DriverS ); } +#[cfg(feature = "driver-vm")] 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(); @@ -193,6 +236,7 @@ fn apply_vm_runtime_defaults(cfg: &mut VmComputeConfig, context: DriverStartupCo ); } +#[cfg(feature = "driver-podman")] fn apply_podman_env_overrides(podman: &mut PodmanComputeConfig) { if let Ok(p) = std::env::var("OPENSHELL_PODMAN_SOCKET") { podman.socket_path = PathBuf::from(p); @@ -221,6 +265,11 @@ fn validate_remote_driver_config(cfg: &RemoteDriverConfig, name: &str) -> Result ))) } +#[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" +))] fn apply_guest_tls_defaults_to_split_fields( ca: &mut Option, cert: &mut Option, @@ -255,14 +304,22 @@ mod tests { ) -> DriverStartupContext<'a> { DriverStartupContext { file, + #[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" + ))] guest_tls: None, + #[cfg(any(feature = "driver-podman", feature = "driver-vm"))] gateway_port: openshell_core::config::DEFAULT_SERVER_PORT, + #[cfg(feature = "driver-vm")] gateway_tls_enabled: false, endpoint_overrides, } } #[test] + #[cfg(feature = "driver-kubernetes")] 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]")); @@ -274,6 +331,7 @@ mod tests { } #[test] + #[cfg(feature = "driver-kubernetes")] fn k8s_sa_bootstrap_uses_configured_namespace_and_service_account() { let file: config_file::ConfigFile = toml::from_str( r#" @@ -292,6 +350,7 @@ service_account_name = "sandbox-sa" } #[test] + #[cfg(feature = "driver-podman")] fn podman_config_reads_bind_mount_opt_in_from_driver_table() { let file: config_file::ConfigFile = toml::from_str( r" @@ -307,6 +366,7 @@ enable_bind_mounts = true } #[test] + #[cfg(feature = "driver-docker")] fn docker_config_reads_bind_mount_opt_in_from_driver_table() { let file: config_file::ConfigFile = toml::from_str( r" @@ -383,6 +443,7 @@ socket_path = "/run/openshell/kyma.sock" } #[test] + #[cfg(feature = "driver-docker")] fn docker_config_reports_selected_invalid_driver_table() { let file: config_file::ConfigFile = toml::from_str( r" @@ -401,6 +462,7 @@ unknown_docker_key = true } #[test] + #[cfg(feature = "driver-vm")] fn vm_config_reports_selected_invalid_driver_table() { let file: config_file::ConfigFile = toml::from_str( r#" diff --git a/crates/openshell-server/src/compute/mod.rs b/crates/openshell-server/src/compute/mod.rs index 3a92cd209a..5362e196fe 100644 --- a/crates/openshell-server/src/compute/mod.rs +++ b/crates/openshell-server/src/compute/mod.rs @@ -5,11 +5,16 @@ pub mod driver_config; pub mod lease; +#[cfg(feature = "driver-vm")] pub mod vm; +#[cfg(feature = "driver-docker")] pub use openshell_driver_docker::DockerComputeConfig; +#[cfg(feature = "driver-kubernetes")] pub use openshell_driver_kubernetes::KubernetesComputeConfig; +#[cfg(feature = "driver-podman")] pub use openshell_driver_podman::PodmanComputeConfig; +#[cfg(feature = "driver-vm")] pub use vm::VmComputeConfig; use crate::grpc::policy::SANDBOX_SETTINGS_OBJECT_TYPE; @@ -34,10 +39,13 @@ use openshell_core::proto::{ PlatformEvent, Sandbox, SandboxCondition, SandboxPhase, SandboxSpec, SandboxStatus, SandboxTemplate, SshSession, }; +#[cfg(feature = "driver-docker")] use openshell_driver_docker::DockerComputeDriver; +#[cfg(feature = "driver-kubernetes")] use openshell_driver_kubernetes::{ ComputeDriverService as KubernetesDriverService, KubernetesComputeDriver, }; +#[cfg(feature = "driver-podman")] use openshell_driver_podman::{ComputeDriverService as PodmanDriverService, PodmanComputeDriver}; use prost::Message; use std::fmt; @@ -66,6 +74,7 @@ trait ShutdownCleanup: Send + Sync { } #[tonic::async_trait] +#[cfg(feature = "driver-docker")] impl ShutdownCleanup for DockerComputeDriver { async fn cleanup_on_shutdown(&self) -> Result<(), String> { let stopped = self @@ -91,6 +100,7 @@ trait StartupResume: Send + Sync { } #[tonic::async_trait] +#[cfg(feature = "driver-docker")] impl StartupResume for DockerComputeDriver { async fn resume_sandbox(&self, sandbox_id: &str, sandbox_name: &str) -> Result { Self::resume_sandbox(self, sandbox_id, sandbox_name) @@ -115,6 +125,7 @@ pub struct ManagedDriverProcess { } impl ManagedDriverProcess { + #[cfg(feature = "driver-vm")] pub(crate) fn new(child: tokio::process::Child, socket_path: PathBuf) -> Self { Self { child: std::sync::Mutex::new(Some(child)), @@ -140,6 +151,7 @@ pub struct AcquiredRemoteDriverEndpoint { } impl AcquiredRemoteDriverEndpoint { + #[cfg(feature = "driver-vm")] pub(crate) fn managed_builtin( driver_kind: ComputeDriverKind, channel: Channel, @@ -336,6 +348,7 @@ impl ComputeRuntime { self.sync_lock.clone().lock_owned().await } + #[cfg(feature = "driver-docker")] pub async fn new_docker( config: openshell_core::Config, docker_config: DockerComputeConfig, @@ -370,6 +383,7 @@ impl ComputeRuntime { .await } + #[cfg(feature = "driver-kubernetes")] pub async fn new_kubernetes( config: KubernetesComputeConfig, store: Arc, @@ -423,6 +437,7 @@ impl ComputeRuntime { .await } + #[cfg(feature = "driver-podman")] pub async fn new_podman( config: PodmanComputeConfig, store: Arc, diff --git a/crates/openshell-server/src/compute/vm.rs b/crates/openshell-server/src/compute/vm.rs index be88047f33..0ca21bb10e 100644 --- a/crates/openshell-server/src/compute/vm.rs +++ b/crates/openshell-server/src/compute/vm.rs @@ -56,6 +56,17 @@ use tonic::transport::Endpoint; #[cfg(unix)] use tower::service_fn; +/// Compile-out guard: greppable proof `driver-vm` is compiled in. +/// +/// `tasks/scripts/verify-drivers-compiled-out.sh` asserts this string is +/// present when `--features driver-vm` is on and absent when it is off. +/// The VM driver runs out-of-process and has no in-server crate dependency, +/// so this marker lives with the launcher plumbing gated by the feature. +/// `#[used]` prevents dead-code elimination. Do not remove without +/// updating the verify script. +#[used] +static COMPILE_MARKER: &str = "OPENSHELL_DRIVER_MARKER:vm"; + const DRIVER_BIN_NAME: &str = "openshell-driver-vm"; const COMPUTE_DRIVER_SOCKET_RUN_DIR: &str = "run"; const COMPUTE_DRIVER_SOCKET_NAME: &str = "compute-driver.sock"; diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 6462ccbbf3..611e822ad3 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -80,6 +80,11 @@ use tracing_bus::TracingLogBus; pub(crate) struct ServerStartupConfig { pub config: Config, pub config_file: Option, + #[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" + ))] pub guest_tls: Option, } @@ -143,6 +148,7 @@ pub struct ServerState { /// Optional K8s `ServiceAccount` authenticator that backs the /// `IssueSandboxToken` bootstrap path. Only present when the gateway /// runs in-cluster. + #[cfg(feature = "driver-kubernetes")] pub k8s_sa_authenticator: Option>, /// Gateway-wide gRPC request rate limiter shared by every multiplex path. @@ -195,6 +201,7 @@ impl ServerState { oidc_cache, sandbox_jwt_issuer: None, sandbox_jwt_authenticator: None, + #[cfg(feature = "driver-kubernetes")] k8s_sa_authenticator: None, grpc_rate_limiter, } @@ -215,6 +222,11 @@ pub(crate) async fn run_server( let ServerStartupConfig { config, config_file, + #[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" + ))] guest_tls, } = startup; @@ -248,8 +260,15 @@ pub(crate) async fn run_server( let supervisor_sessions = Arc::new(supervisor_session::SupervisorSessionRegistry::new()); let driver_startup = compute::driver_config::DriverStartupContext { file: config_file.as_ref(), + #[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" + ))] guest_tls: guest_tls.as_ref(), + #[cfg(any(feature = "driver-podman", feature = "driver-vm"))] gateway_port: config.bind_address.port(), + #[cfg(feature = "driver-vm")] gateway_tls_enabled: config.tls.is_some(), endpoint_overrides: &config.compute_driver_endpoints, }; @@ -330,6 +349,7 @@ pub(crate) async fn run_server( // env var) and has a sandbox JWT issuer to mint replacements against; // outside the cluster we can't call the apiserver's TokenReview API, // and without the issuer there's nothing to exchange the SA token for. + #[cfg(feature = "driver-kubernetes")] 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. @@ -740,6 +760,7 @@ async fn build_compute_runtime( info!(driver = %driver.name(), "Using compute driver"); let runtime = match driver { + #[cfg(feature = "driver-kubernetes")] ConfiguredComputeDriver::Builtin(ComputeDriverKind::Kubernetes) => { warn_if_kubernetes_sandbox_jwt_expiry_disabled(config); let k8s_config = @@ -754,6 +775,7 @@ async fn build_compute_runtime( ) .await } + #[cfg(feature = "driver-docker")] ConfiguredComputeDriver::Builtin(ComputeDriverKind::Docker) => { let docker_config = compute::driver_config::docker_config_from_context(driver_startup)?; ComputeRuntime::new_docker( @@ -767,6 +789,7 @@ async fn build_compute_runtime( ) .await } + #[cfg(feature = "driver-podman")] ConfiguredComputeDriver::Builtin(ComputeDriverKind::Podman) => { let podman_config = compute::driver_config::podman_config_from_context(driver_startup)?; ComputeRuntime::new_podman( @@ -779,6 +802,7 @@ async fn build_compute_runtime( ) .await } + #[cfg(feature = "driver-vm")] ConfiguredComputeDriver::Builtin(ComputeDriverKind::Vm) => { let vm_config = compute::driver_config::vm_config_from_context(driver_startup)?; let endpoint = compute::vm::spawn(config, &vm_config).await?; @@ -813,6 +837,16 @@ async fn build_compute_runtime( ) .await } + // Unreachable at runtime: config validation in `configured_compute_driver` + // rejects builtins whose feature was compiled out, so a `Builtin` variant + // reaching here means validation and dispatch got out of sync. + // `#[allow(unreachable_patterns)]` silences the lint in the default + // build where every concrete arm above is compiled in and this + // catch-all is unreachable at compile time. + #[allow(unreachable_patterns)] + _ => unreachable!( + "configured compute driver passed startup validation but is not compiled into this gateway" + ), }; runtime.map_err(|e| Error::execution(format!("failed to create compute runtime: {e}"))) @@ -838,14 +872,16 @@ fn configured_compute_driver( driver_startup: compute::driver_config::DriverStartupContext<'_>, ) -> Result { match config.compute_drivers.as_slice() { - [] => match openshell_core::config::detect_driver() { + [] => match filtered_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", + set --drivers or OPENSHELL_DRIVERS to a supported driver, \ + or to an extension driver name with a matching \ + [openshell.drivers.].socket_path", )), }, [driver] => resolve_configured_compute_driver(driver, driver_startup), @@ -870,16 +906,46 @@ fn resolve_configured_compute_driver( } if let Some(kind) = driver_kind { + if !driver_compiled_in(kind) { + return Err(Error::config(format!( + "compute driver '{name}' is not supported by this gateway build" + ))); + } return Ok(ConfiguredComputeDriver::Builtin(kind)); } Ok(ConfiguredComputeDriver::Remote { name }) } +/// Whether the built-in driver `kind` was compiled into this gateway. +/// +/// Third-party drivers reached via `--compute-driver-socket` are handled +/// through the remote path and are always available regardless of features. +const fn driver_compiled_in(kind: ComputeDriverKind) -> bool { + match kind { + ComputeDriverKind::Kubernetes => cfg!(feature = "driver-kubernetes"), + ComputeDriverKind::Docker => cfg!(feature = "driver-docker"), + ComputeDriverKind::Podman => cfg!(feature = "driver-podman"), + ComputeDriverKind::Vm => cfg!(feature = "driver-vm"), + } +} + +/// Auto-detect a driver, ignoring any that were compiled out. +/// +/// Wraps [`openshell_core::config::detect_driver`]; if the environment's +/// preferred driver is not compiled in, the caller falls back to the +/// standard "no driver configured" error rather than silently starting a +/// different backend. +fn filtered_detect_driver() -> Option { + let kind = openshell_core::config::detect_driver()?; + driver_compiled_in(kind).then_some(kind) +} + fn builtin_compute_driver(name: &str) -> Option { name.parse().ok() } +#[cfg(feature = "driver-kubernetes")] fn kubernetes_sandbox_jwt_expiry_disabled(config: &Config) -> bool { config .gateway_jwt @@ -887,6 +953,7 @@ fn kubernetes_sandbox_jwt_expiry_disabled(config: &Config) -> bool { .is_some_and(|jwt| jwt.ttl_secs == 0) } +#[cfg(feature = "driver-kubernetes")] fn warn_if_kubernetes_sandbox_jwt_expiry_disabled(config: &Config) { if kubernetes_sandbox_jwt_expiry_disabled(config) { warn!( @@ -897,11 +964,12 @@ fn warn_if_kubernetes_sandbox_jwt_expiry_disabled(config: &Config) { #[cfg(test)] mod tests { + #[cfg(feature = "driver-kubernetes")] + use super::kubernetes_sandbox_jwt_expiry_disabled; use super::{ ConfiguredComputeDriver, ConnectionProtocol, MultiplexService, ServerState, TlsAcceptor, allow_plaintext_service_http, classify_initial_bytes, configured_compute_driver, - gateway_listener_addresses, is_benign_tls_handshake_failure, - kubernetes_sandbox_jwt_expiry_disabled, serve_gateway_listener, + gateway_listener_addresses, is_benign_tls_handshake_failure, serve_gateway_listener, }; use openshell_core::{ ComputeDriverKind, Config, @@ -924,8 +992,15 @@ mod tests { ) -> crate::compute::driver_config::DriverStartupContext<'a> { crate::compute::driver_config::DriverStartupContext { file, + #[cfg(any( + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm" + ))] guest_tls: None, + #[cfg(any(feature = "driver-podman", feature = "driver-vm"))] gateway_port: openshell_core::config::DEFAULT_SERVER_PORT, + #[cfg(feature = "driver-vm")] gateway_tls_enabled: false, endpoint_overrides: &config.compute_driver_endpoints, } @@ -1253,6 +1328,24 @@ mod tests { } } + #[test] + #[cfg(not(any( + feature = "driver-kubernetes", + feature = "driver-docker", + feature = "driver-podman", + feature = "driver-vm", + )))] + fn configured_compute_driver_empty_points_at_extensions_when_no_builtins() { + let config = Config::new(None).with_compute_drivers(std::iter::empty::()); + let err = + configured_compute_driver(&config, test_driver_startup(&config, None)).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("socket_path"), + "expected extension socket_path guidance, got: {msg}" + ); + } + #[test] fn configured_compute_driver_rejects_multiple_entries() { let config = Config::new(None) @@ -1267,6 +1360,7 @@ mod tests { } #[test] + #[cfg(feature = "driver-podman")] fn configured_compute_driver_accepts_podman() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Podman]); let driver = @@ -1278,6 +1372,7 @@ mod tests { } #[test] + #[cfg(feature = "driver-vm")] fn configured_compute_driver_accepts_vm() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Vm]); let driver = @@ -1289,6 +1384,7 @@ mod tests { } #[test] + #[cfg(feature = "driver-docker")] fn configured_compute_driver_accepts_docker() { let config = Config::new(None).with_compute_drivers([ComputeDriverKind::Docker]); let driver = @@ -1349,6 +1445,7 @@ mod tests { } #[test] + #[cfg(feature = "driver-kubernetes")] 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); @@ -1392,4 +1489,55 @@ mod tests { vec![primary, docker] ); } + + /// Exercise both directions of the compile-time driver gate. A driver + /// compiled *in* must resolve to `ConfiguredComputeDriver::Builtin(kind)`; + /// a driver compiled *out* must fail with a message that names the + /// specific driver and states it is not supported by this gateway build. + #[test] + fn configured_compute_driver_matches_compiled_features() { + // Exhaustive match to ensure that the test is updated if a new driver is added. + fn compiled_in(kind: ComputeDriverKind) -> bool { + match kind { + ComputeDriverKind::Kubernetes => cfg!(feature = "driver-kubernetes"), + ComputeDriverKind::Docker => cfg!(feature = "driver-docker"), + ComputeDriverKind::Podman => cfg!(feature = "driver-podman"), + ComputeDriverKind::Vm => cfg!(feature = "driver-vm"), + } + } + + let driver_features: std::collections::HashMap = [ + ComputeDriverKind::Kubernetes, + ComputeDriverKind::Docker, + ComputeDriverKind::Podman, + ComputeDriverKind::Vm, + ] + .into_iter() + .map(|kind| (kind, compiled_in(kind))) + .collect(); + + for (&kind, &present) in &driver_features { + let config = Config::new(None).with_compute_drivers([kind]); + let result = configured_compute_driver(&config, test_driver_startup(&config, None)); + + if present { + let driver = result.unwrap(); + assert!( + matches!(driver, ConfiguredComputeDriver::Builtin(k) if k == kind), + "driver {kind:?}: expected Builtin({kind:?}), got {driver:?}" + ); + } else { + let err = result.expect_err("must reject compiled-out driver"); + let msg = err.to_string(); + assert!( + msg.contains("not supported by this gateway build"), + "driver {kind:?}: unexpected error: {msg}" + ); + assert!( + msg.contains(kind.as_str()), + "driver {kind:?}: error must name the specific driver: {msg}" + ); + } + } + } } diff --git a/crates/openshell-server/src/multiplex.rs b/crates/openshell-server/src/multiplex.rs index 9e70c64721..d198773956 100644 --- a/crates/openshell-server/src/multiplex.rs +++ b/crates/openshell-server/src/multiplex.rs @@ -447,6 +447,7 @@ where /// to pass-through unless mTLS or local unauthenticated users are enabled. fn build_authenticator_chain(state: &ServerState) -> Option { let mut authenticators: Vec> = Vec::new(); + #[cfg(feature = "driver-kubernetes")] if let Some(k8s) = state.k8s_sa_authenticator.clone() { authenticators.push(k8s); } diff --git a/crates/openshell-server/src/supervisor_session.rs b/crates/openshell-server/src/supervisor_session.rs index 4adf9e8b6f..5698b481ba 100644 --- a/crates/openshell-server/src/supervisor_session.rs +++ b/crates/openshell-server/src/supervisor_session.rs @@ -63,6 +63,7 @@ struct LiveSession { /// target-open failure reported by the supervisor. type RelayStreamSender = oneshot::Sender>; +#[cfg(feature = "driver-docker")] impl openshell_driver_docker::SupervisorReadiness for SupervisorSessionRegistry { fn is_supervisor_connected(&self, sandbox_id: &str) -> bool { Self::is_connected(self, sandbox_id) diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 88b82870de..e2ec9935b5 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -340,3 +340,47 @@ compute_drivers = ["kyma"] [openshell.drivers.kyma] socket_path = "/run/openshell/kyma-compute-driver.sock" ``` + +## Choosing Which Drivers Are Compiled In + +Each built-in compute driver is gated by a Cargo feature on +`openshell-server`, all enabled by default. Operators who only need one +driver can produce a smaller gateway with a reduced supply-chain surface +by building `--no-default-features` and selecting a subset: + +| Feature | Compiles in | +|---|---| +| `driver-kubernetes` | Kubernetes driver, K8s ServiceAccount authenticator, `kube-rs` and K8s client dependencies. | +| `driver-docker` | Docker driver, `bollard` and Docker-specific supervisor readiness plumbing. | +| `driver-podman` | Podman driver and libpod REST client. | +| `driver-vm` | In-server plumbing for spawning `openshell-driver-vm` and speaking to it over UDS. The driver binary itself is packaged separately. | +| `telemetry` | Anonymous telemetry emission. | + +Kubernetes-only gateway: + +```shell +cargo build --release -p openshell-server --bin openshell-gateway \ + --no-default-features --features driver-kubernetes,telemetry +``` + +Docker + Podman gateway (drops K8s and VM code and their transitive +dependencies): + +```shell +cargo build --release -p openshell-server --bin openshell-gateway \ + --no-default-features --features driver-docker,driver-podman,telemetry +``` + +Extension-only gateway (no in-tree drivers; talks only to an out-of-tree +driver over a Unix socket) with disabled telemetry: + +```shell +cargo build --release -p openshell-server --bin openshell-gateway --no-default-features +``` + +This build carries no built-in driver code. `compute_drivers` must name an +extension driver with a matching `[openshell.drivers.].socket_path`. +Configuring a driver in `gateway.toml` that this gateway was not built with fails at startup with a +message naming both the driver and the Cargo flag that re-enables it. +Extension drivers reached via `--compute-driver-socket` do not depend on any +driver feature and work regardless. diff --git a/tasks/rust.toml b/tasks/rust.toml index a8856377fe..4ffb0061b7 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -33,9 +33,89 @@ run = [ # markers, so the absent checks below can never become silently vacuous. "cargo build -p openshell-server --bin openshell-gateway", "tasks/scripts/verify-telemetry-compiled-out.sh present target/debug/openshell-gateway", - # Guard: telemetry-free builds must contain no telemetry markers. - "cargo build -p openshell-server --bin openshell-gateway --no-default-features", + # Guard: telemetry-free builds must contain no telemetry markers. The + # gateway needs at least one compute driver to link, so build with a + # single-driver feature set that excludes telemetry. + "cargo build -p openshell-server --bin openshell-gateway --no-default-features --features driver-kubernetes,driver-docker,driver-podman,driver-vm", "tasks/scripts/verify-telemetry-compiled-out.sh absent target/debug/openshell-gateway", "cargo build -p openshell-sandbox --bin openshell-sandbox --no-default-features", "tasks/scripts/verify-telemetry-compiled-out.sh absent target/debug/openshell-sandbox", ] + +["rust:verify:drivers-off"] +description = "Verify each compute driver can be compiled out via per-driver cargo features" +depends = [ + # Positive control: the default build carries every driver's markers. + "rust:verify:drivers-off:default-all", + # Each single-driver build carries only that driver's markers. + "rust:verify:drivers-off:kubernetes", + "rust:verify:drivers-off:docker", + "rust:verify:drivers-off:podman", + "rust:verify:drivers-off:vm", + # Mixed feature set: verifies driver features compose additively. + "rust:verify:drivers-off:docker+podman", + # Zero-driver build is a valid extension-only gateway shape. Asserts the + # binary links, no built-in driver markers survived, and the extension-only + # unit-test paths behind #[cfg(not(any(feature = "driver-*")))] execute. + "rust:verify:drivers-off:none", +] + +["rust:verify:drivers-off:default-all"] +description = "Verify the default gateway build carries every driver's markers" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway", + "tasks/scripts/verify-drivers-compiled-out.sh present all target/debug/openshell-gateway", +] + +["rust:verify:drivers-off:kubernetes"] +description = "Verify a --features driver-kubernetes gateway carries only the kubernetes marker" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway --no-default-features --features driver-kubernetes", + "tasks/scripts/verify-drivers-compiled-out.sh only kubernetes target/debug/openshell-gateway", +] + +["rust:verify:drivers-off:docker"] +description = "Verify a --features driver-docker gateway carries only the docker marker" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway --no-default-features --features driver-docker", + "tasks/scripts/verify-drivers-compiled-out.sh only docker target/debug/openshell-gateway", +] + +["rust:verify:drivers-off:podman"] +description = "Verify a --features driver-podman gateway carries only the podman marker" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway --no-default-features --features driver-podman", + "tasks/scripts/verify-drivers-compiled-out.sh only podman target/debug/openshell-gateway", +] + +["rust:verify:drivers-off:vm"] +description = "Verify a --features driver-vm gateway carries only the vm marker" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway --no-default-features --features driver-vm", + "tasks/scripts/verify-drivers-compiled-out.sh only vm target/debug/openshell-gateway", +] + +["rust:verify:drivers-off:docker+podman"] +description = "Verify a --features driver-docker,driver-podman gateway carries only those two markers" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway --no-default-features --features driver-docker,driver-podman", + "tasks/scripts/verify-drivers-compiled-out.sh only docker,podman target/debug/openshell-gateway", +] + +["rust:verify:drivers-off:none"] +description = "Verify a --no-default-features gateway links, carries no driver markers, and runs the extension-only unit tests" +hide = true +run = [ + "cargo build -p openshell-server --bin openshell-gateway --no-default-features", + "tasks/scripts/verify-drivers-compiled-out.sh absent all target/debug/openshell-gateway", + # The zero-driver profile gates the extension-only unit test paths; run + # the library test suite under --no-default-features so those assertions + # actually execute. + "cargo test -p openshell-server --lib --no-default-features", +] diff --git a/tasks/scripts/verify-drivers-compiled-out.sh b/tasks/scripts/verify-drivers-compiled-out.sh new file mode 100755 index 0000000000..36055f27b7 --- /dev/null +++ b/tasks/scripts/verify-drivers-compiled-out.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Verify whether compute-driver code is present in a compiled binary. +# +# Per-driver Cargo features (driver-kubernetes, driver-docker, driver-podman, +# driver-vm) on openshell-server, all default-on, gate the driver crates and +# their in-server plumbing. Building with --no-default-features and enabling +# only a subset must produce a binary that carries only those drivers. This +# guard inspects a built binary for markers that only exist when a given +# driver is compiled in. +# +# Markers are strings baked into each driver's code and must not overlap +# with other drivers or shared code. The `present` positive control fails +# loudly if a marker goes stale, so `absent` checks can never become +# silently vacuous. + +set -euo pipefail + +# Marker table: DRIVER=marker_that_only_appears_when_that_driver_is_compiled_in +# +# Each driver crate declares a `#[used] static COMPILE_MARKER` holding a +# well-known byte string; the VM driver's marker lives in compute::vm behind +# `#[cfg(feature = "driver-vm")]`. `#[used]` prevents dead-code elimination +# so the marker survives every optimization level and strip mode. Keep these +# in sync with the driver crates. +declare -A MARKERS=( + [kubernetes]="OPENSHELL_DRIVER_MARKER:kubernetes" + [docker]="OPENSHELL_DRIVER_MARKER:docker" + [podman]="OPENSHELL_DRIVER_MARKER:podman" + [vm]="OPENSHELL_DRIVER_MARKER:vm" +) + +ALL_DRIVERS=(kubernetes docker podman vm) + +usage() { + cat >&2 <<'EOF' +Usage: + verify-drivers-compiled-out.sh present + Assert the driver's markers ARE present. Use as a positive control. + verify-drivers-compiled-out.sh absent + Assert the driver's markers are NOT present. + verify-drivers-compiled-out.sh only + Assert markers are present for each listed driver AND absent for + each unlisted one. Covers presence + absence in a single pass. + +Drivers: kubernetes docker podman vm (or 'all') +EOF +} + +if [[ $# -lt 3 ]]; then + usage + exit 2 +fi + +mode=$1 +selector=$2 +binary=$3 + +if [[ ! -f $binary ]]; then + echo "error: binary not found: $binary" >&2 + exit 2 +fi +if ! command -v strings >/dev/null 2>&1; then + echo "error: 'strings' (binutils) is required to inspect the binary" >&2 + exit 2 +fi + +dump=$(strings -a "$binary") +failed=0 + +# Assert that $1 marker appears at least once in $dump for driver $2. +assert_present() { + local driver=$1 + local marker=${MARKERS[$driver]} + local count + count=$(grep -c -F "$marker" <<<"$dump" || true) + if [[ $count -eq 0 ]]; then + echo "FAIL: driver '$driver' marker '$marker' missing from $(basename "$binary")" >&2 + failed=1 + else + echo "OK: driver '$driver' compiled in ($count occurrence(s) of '$marker')" + fi +} + +# Assert that $1 marker does NOT appear in $dump for driver $2. +assert_absent() { + local driver=$1 + local marker=${MARKERS[$driver]} + local count + count=$(grep -c -F "$marker" <<<"$dump" || true) + if [[ $count -ne 0 ]]; then + echo "FAIL: driver '$driver' marker '$marker' found in $(basename "$binary") ($count occurrence(s)); driver was not compiled out" >&2 + failed=1 + else + echo "OK: driver '$driver' compiled out (0 occurrences of '$marker')" + fi +} + +# Expand 'all' or a comma-separated list into the SELECTED array in the +# caller's scope. Exits 2 on any unknown driver. Runs in the current shell +# so that exit propagates — do not call this from a subshell or process +# substitution. +resolve_drivers() { + local input=$1 + SELECTED=() + if [[ $input == "all" ]]; then + SELECTED=("${ALL_DRIVERS[@]}") + return + fi + local drivers + IFS=',' read -r -a drivers <<<"$input" + for d in "${drivers[@]}"; do + if [[ -z ${MARKERS[$d]+set} ]]; then + echo "error: unknown driver '$d'. known: ${ALL_DRIVERS[*]}" >&2 + exit 2 + fi + SELECTED+=("$d") + done +} + +resolve_drivers "$selector" + +case "$mode" in + present) + for d in "${SELECTED[@]}"; do + assert_present "$d" + done + ;; + absent) + for d in "${SELECTED[@]}"; do + assert_absent "$d" + done + ;; + only) + declare -A present_set=() + for d in "${SELECTED[@]}"; do + present_set[$d]=1 + done + for d in "${ALL_DRIVERS[@]}"; do + if [[ -n ${present_set[$d]+set} ]]; then + assert_present "$d" + else + assert_absent "$d" + fi + done + ;; + *) + usage + exit 2 + ;; +esac + +exit "$failed"