Skip to content

Commit e77e98c

Browse files
committed
refactor(compute): register compiled drivers
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 2c0adf4 commit e77e98c

9 files changed

Lines changed: 868 additions & 236 deletions

File tree

‎.agents/skills/debug-openshell-cluster/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ The target deployment flow is:
1919
4. The CLI registers a reachable gateway endpoint with `openshell gateway add`.
2020
5. The gateway creates sandboxes through the selected compute driver.
2121

22+
The standard gateway binary explicitly installs its compiled Docker, Podman,
23+
Kubernetes, and VM registrations at startup. With no configured driver, the
24+
gateway probes only installed registrations in priority order (Kubernetes,
25+
Podman, then Docker); VM has no probe and remains opt-in. A custom gateway
26+
binary may install a different set, so confirm the binary's registered drivers
27+
when auto-detection reports that no suitable driver is available.
28+
2229
For local evaluation only, TLS may be disabled and the gateway can be reached through `http://127.0.0.1:<port>`.
2330

2431
## Prerequisites

‎architecture/compute-runtimes.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ The gateway records driver identity and version from the startup capability
9191
response. Elevated gateway info reports that initialized driver snapshot instead
9292
of re-querying drivers on each request.
9393

94+
## Compiled Driver Selection
95+
96+
The gateway binary explicitly installs the compute drivers compiled into that
97+
binary before entering server startup. The server selects a configured driver
98+
by normalized registry name. When no driver is configured, it evaluates only
99+
the installed drivers' probes in registered priority order, records every
100+
available registration, and selects the first. Drivers without a probe,
101+
including VM, remain opt-in.
102+
103+
Startup computes this selection once after merging configuration. The same
104+
selection drives authentication defaults and runtime construction, so a probe
105+
result cannot change which driver is constructed later in startup.
106+
107+
This follows the same composition model as SQLx's `Any` drivers: the binary
108+
defines the available implementation set, while the runtime consumes a generic
109+
registry. Adding or removing a compiled driver therefore changes registration
110+
rather than the server's selection flow. Alternate gateway binaries can install
111+
their own `ComputeDriverFactory` registrations and hand the completed registry
112+
to `run_cli_with_compute_drivers`; factories receive merged driver config and
113+
finish through the same in-process runtime adapter. A configured UDS endpoint
114+
still takes precedence over a compiled registration with the same name.
115+
94116
## Stop and Start Lifecycle
95117

96118
The gateway persists lifecycle intent before mutating compute:

‎crates/openshell-core/src/config.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,9 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
208208
None
209209
}
210210

211-
fn is_podman_available() -> bool {
211+
/// Return whether a responsive local Podman API socket is available.
212+
#[must_use]
213+
pub fn is_podman_available() -> bool {
212214
detect_podman_socket().is_some()
213215
}
214216

@@ -266,7 +268,9 @@ fn podman_socket_candidates_from_env(
266268
candidates
267269
}
268270

269-
fn is_docker_available() -> bool {
271+
/// Return whether a responsive local Docker API socket is available.
272+
#[must_use]
273+
pub fn is_docker_available() -> bool {
270274
detect_docker_socket().is_some()
271275
}
272276

‎crates/openshell-server/src/cli.rs‎

Lines changed: 139 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use crate::compute::driver_config::GuestTlsPaths;
1818
use crate::config_file::{self, ConfigFile, GatewayFileSection};
1919
use crate::defaults::{self, LocalTlsPaths};
2020
use crate::{
21-
ServerStartupConfig, configured_compute_driver_for_startup, run_server,
22-
tracing_bus::TracingLogBus,
21+
ComputeDriverRegistry, ServerStartupConfig, configured_compute_driver_for_startup,
22+
install_default_compute_drivers, run_server, tracing_bus::TracingLogBus,
2323
};
2424

2525
/// `OpenShell` gateway process - gRPC and HTTP server with protocol multiplexing.
@@ -223,6 +223,11 @@ pub fn command() -> Command {
223223
}
224224

225225
pub async fn run_cli() -> Result<()> {
226+
run_cli_with_compute_drivers(install_default_compute_drivers()).await
227+
}
228+
229+
/// Run the gateway CLI with the compute drivers linked by the binary.
230+
pub async fn run_cli_with_compute_drivers(compute_drivers: ComputeDriverRegistry) -> Result<()> {
226231
rustls::crypto::ring::default_provider()
227232
.install_default()
228233
.map_err(|e| miette::miette!("failed to install rustls crypto provider: {e:?}"))?;
@@ -232,11 +237,15 @@ pub async fn run_cli() -> Result<()> {
232237

233238
match cli.command {
234239
Some(Commands::GenerateCerts(args)) => certgen::run(args).await,
235-
None => Box::pin(run_from_args(cli.run, matches)).await,
240+
None => Box::pin(run_from_args(cli.run, matches, compute_drivers)).await,
236241
}
237242
}
238243

239-
fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result<ServerStartupConfig> {
244+
fn prepare_server_config(
245+
args: &mut RunArgs,
246+
matches: &ArgMatches,
247+
compute_drivers: &ComputeDriverRegistry,
248+
) -> Result<ServerStartupConfig> {
240249
// Load TOML when explicitly requested, or from the default XDG location
241250
// when that file exists. Missing default config is not an error: runtime
242251
// defaults and OPENSHELL_* env vars are enough for package-managed starts.
@@ -250,6 +259,10 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result<Ser
250259
merge_file_into_args(args, &file.openshell.gateway, matches);
251260
}
252261
normalize_compute_driver_socket_args(args, matches)?;
262+
let compute_driver = compute_drivers
263+
.select(&args.drivers)
264+
.map_err(|error| miette::miette!("{error}"))?;
265+
let compute_driver_kind = compute_driver.name().parse::<ComputeDriverKind>().ok();
253266

254267
let local_tls = apply_runtime_defaults(args)?;
255268
let guest_tls = local_tls.as_ref().map(GuestTlsPaths::from);
@@ -259,7 +272,8 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result<Ser
259272

260273
let has_client_ca = args.tls_client_ca.is_some();
261274
let has_oidc = args.oidc_issuer.is_some();
262-
let mtls_auth_enabled = resolve_mtls_auth_enabled(args, matches, file.as_ref());
275+
let mtls_auth_enabled =
276+
resolve_mtls_auth_enabled(args, matches, file.as_ref(), compute_driver_kind);
263277

264278
if args.disable_tls && has_client_ca {
265279
return Err(miette::miette!(
@@ -276,12 +290,7 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result<Ser
276290
"mTLS user authentication requires --tls-client-ca so client certificates can be verified."
277291
));
278292
}
279-
if mtls_auth_enabled
280-
&& matches!(
281-
effective_single_driver(args),
282-
Some(ComputeDriverKind::Kubernetes)
283-
)
284-
{
293+
if mtls_auth_enabled && matches!(compute_driver_kind, Some(ComputeDriverKind::Kubernetes)) {
285294
return Err(miette::miette!(
286295
"mTLS user authentication is not supported with the Kubernetes compute driver. Configure OIDC or a trusted fronting proxy for user authentication."
287296
));
@@ -469,12 +478,17 @@ fn prepare_server_config(args: &mut RunArgs, matches: &ArgMatches) -> Result<Ser
469478
config,
470479
config_file: file,
471480
guest_tls,
481+
compute_driver,
472482
})
473483
}
474484

475-
async fn run_from_args(mut args: RunArgs, matches: ArgMatches) -> Result<()> {
476-
let prepared = prepare_server_config(&mut args, &matches)?;
477-
let compute_driver = configured_compute_driver_for_startup(&prepared)?;
485+
async fn run_from_args(
486+
mut args: RunArgs,
487+
matches: ArgMatches,
488+
compute_drivers: ComputeDriverRegistry,
489+
) -> Result<()> {
490+
let prepared = prepare_server_config(&mut args, &matches, &compute_drivers)?;
491+
let compute_driver = configured_compute_driver_for_startup(&compute_drivers, &prepared)?;
478492

479493
let tracing_log_bus = TracingLogBus::new();
480494
let otlp_config = prepared
@@ -779,17 +793,9 @@ fn normalize_compute_driver_socket_args(args: &mut RunArgs, matches: &ArgMatches
779793
}
780794
}
781795

782-
fn effective_single_driver(args: &RunArgs) -> Option<ComputeDriverKind> {
783-
match args.drivers.as_slice() {
784-
[] => openshell_core::config::detect_driver(),
785-
[driver] => driver.parse().ok(),
786-
_ => None,
787-
}
788-
}
789-
790-
fn is_singleplayer_driver(args: &RunArgs) -> bool {
796+
fn is_singleplayer_driver(driver: Option<ComputeDriverKind>) -> bool {
791797
matches!(
792-
effective_single_driver(args),
798+
driver,
793799
Some(ComputeDriverKind::Docker | ComputeDriverKind::Podman | ComputeDriverKind::Vm)
794800
)
795801
}
@@ -798,6 +804,7 @@ fn resolve_mtls_auth_enabled(
798804
args: &RunArgs,
799805
matches: &ArgMatches,
800806
file: Option<&ConfigFile>,
807+
compute_driver: Option<ComputeDriverKind>,
801808
) -> bool {
802809
let file_configured = file
803810
.and_then(|f| f.openshell.gateway.mtls_auth.as_ref())
@@ -810,7 +817,7 @@ fn resolve_mtls_auth_enabled(
810817
return false;
811818
}
812819

813-
is_singleplayer_driver(args)
820+
is_singleplayer_driver(compute_driver)
814821
}
815822

816823
#[cfg(test)]
@@ -819,6 +826,43 @@ mod tests {
819826
use crate::TEST_ENV_LOCK as ENV_LOCK;
820827
use clap::Parser;
821828
use std::net::{IpAddr, Ipv4Addr};
829+
use std::sync::atomic::{AtomicUsize, Ordering};
830+
831+
static REGISTRY_DETECTION_CALLS: AtomicUsize = AtomicUsize::new(0);
832+
833+
fn detect_registered_docker() -> bool {
834+
REGISTRY_DETECTION_CALLS.fetch_add(1, Ordering::SeqCst);
835+
true
836+
}
837+
838+
#[derive(Clone, Copy)]
839+
struct TestComputeDriverFactory;
840+
841+
#[async_trait::async_trait]
842+
impl crate::ComputeDriverFactory for TestComputeDriverFactory {
843+
async fn build(
844+
&self,
845+
_context: crate::ComputeDriverBuildContext<'_>,
846+
) -> openshell_core::Result<crate::ComputeDriverBuildOutput> {
847+
unreachable!("configuration tests do not construct the driver")
848+
}
849+
}
850+
851+
fn detected_docker_registry() -> crate::ComputeDriverRegistry {
852+
let mut registry = crate::ComputeDriverRegistry::new();
853+
registry
854+
.install(
855+
crate::ComputeDriverRegistration::new(
856+
"docker",
857+
100,
858+
Some(detect_registered_docker),
859+
TestComputeDriverFactory,
860+
)
861+
.unwrap(),
862+
)
863+
.unwrap();
864+
registry
865+
}
822866

823867
struct EnvVarGuard {
824868
key: &'static str,
@@ -1286,7 +1330,47 @@ mod tests {
12861330
"/tmp/ca.crt",
12871331
]);
12881332

1289-
assert!(super::resolve_mtls_auth_enabled(&args, &matches, None));
1333+
assert!(super::resolve_mtls_auth_enabled(
1334+
&args,
1335+
&matches,
1336+
None,
1337+
Some(openshell_core::ComputeDriverKind::Docker)
1338+
));
1339+
}
1340+
1341+
#[test]
1342+
fn registry_detection_drives_auth_defaults_once() {
1343+
let _lock = ENV_LOCK
1344+
.lock()
1345+
.unwrap_or_else(std::sync::PoisonError::into_inner);
1346+
let state = tempfile::tempdir().unwrap();
1347+
let config = tempfile::tempdir().unwrap();
1348+
let _state = EnvVarGuard::set("XDG_STATE_HOME", state.path().to_str().unwrap());
1349+
let _config = EnvVarGuard::set("XDG_CONFIG_HOME", config.path().to_str().unwrap());
1350+
let _kubernetes = EnvVarGuard::set("KUBERNETES_SERVICE_HOST", "10.0.0.1");
1351+
let _mtls = EnvVarGuard::remove("OPENSHELL_ENABLE_MTLS_AUTH");
1352+
let _drivers = EnvVarGuard::remove("OPENSHELL_DRIVERS");
1353+
REGISTRY_DETECTION_CALLS.store(0, Ordering::SeqCst);
1354+
1355+
let (mut args, matches) = parse_with_args(&[
1356+
"openshell-gateway",
1357+
"--db-url",
1358+
"sqlite::memory:",
1359+
"--tls-cert",
1360+
"/tmp/server.crt",
1361+
"--tls-key",
1362+
"/tmp/server.key",
1363+
"--tls-client-ca",
1364+
"/tmp/ca.crt",
1365+
]);
1366+
let registry = detected_docker_registry();
1367+
1368+
let prepared = super::prepare_server_config(&mut args, &matches, &registry).unwrap();
1369+
1370+
assert_eq!(prepared.compute_driver.name(), "docker");
1371+
assert!(prepared.config.compute_drivers.is_empty());
1372+
assert!(prepared.config.mtls_auth.enabled);
1373+
assert_eq!(REGISTRY_DETECTION_CALLS.load(Ordering::SeqCst), 1);
12901374
}
12911375

12921376
#[test]
@@ -1310,7 +1394,12 @@ mod tests {
13101394
"/tmp/ca.crt",
13111395
]);
13121396

1313-
assert!(!super::resolve_mtls_auth_enabled(&args, &matches, None));
1397+
assert!(!super::resolve_mtls_auth_enabled(
1398+
&args,
1399+
&matches,
1400+
None,
1401+
Some(openshell_core::ComputeDriverKind::Kubernetes)
1402+
));
13141403
}
13151404

13161405
#[test]
@@ -1345,7 +1434,8 @@ enabled = false
13451434
assert!(!super::resolve_mtls_auth_enabled(
13461435
&args,
13471436
&matches,
1348-
Some(&file)
1437+
Some(&file),
1438+
Some(openshell_core::ComputeDriverKind::Docker)
13491439
));
13501440
}
13511441

@@ -1573,37 +1663,21 @@ ssh_session_ttl_secs = 1234
15731663

15741664
#[test]
15751665
fn singleplayer_driver_matches_only_one_local_driver() {
1576-
for driver in ["docker", "podman", "vm"] {
1577-
let (args, _) = parse_with_args(&[
1578-
"openshell-gateway",
1579-
"--db-url",
1580-
"sqlite::memory:",
1581-
"--drivers",
1582-
driver,
1583-
]);
1666+
for driver in [
1667+
openshell_core::ComputeDriverKind::Docker,
1668+
openshell_core::ComputeDriverKind::Podman,
1669+
openshell_core::ComputeDriverKind::Vm,
1670+
] {
15841671
assert!(
1585-
super::is_singleplayer_driver(&args),
1672+
super::is_singleplayer_driver(Some(driver)),
15861673
"{driver} should be singleplayer"
15871674
);
15881675
}
15891676

1590-
let (k8s, _) = parse_with_args(&[
1591-
"openshell-gateway",
1592-
"--db-url",
1593-
"sqlite::memory:",
1594-
"--drivers",
1595-
"kubernetes",
1596-
]);
1597-
assert!(!super::is_singleplayer_driver(&k8s));
1598-
1599-
let (multi, _) = parse_with_args(&[
1600-
"openshell-gateway",
1601-
"--db-url",
1602-
"sqlite::memory:",
1603-
"--drivers",
1604-
"docker,podman",
1605-
]);
1606-
assert!(!super::is_singleplayer_driver(&multi));
1677+
assert!(!super::is_singleplayer_driver(Some(
1678+
openshell_core::ComputeDriverKind::Kubernetes
1679+
)));
1680+
assert!(!super::is_singleplayer_driver(None));
16071681
}
16081682

16091683
#[test]
@@ -1629,7 +1703,11 @@ ssh_session_ttl_secs = 1234
16291703
Some(std::path::Path::new("/run/openshell/kyma.sock"))
16301704
);
16311705
assert_eq!(args.drivers, ["kyma"]);
1632-
assert!(super::effective_single_driver(&args).is_none());
1706+
assert!(
1707+
args.drivers[0]
1708+
.parse::<openshell_core::ComputeDriverKind>()
1709+
.is_err()
1710+
);
16331711
}
16341712

16351713
#[test]
@@ -1809,8 +1887,12 @@ mem_mib = "not-a-number"
18091887
"--disable-tls",
18101888
]);
18111889

1812-
let prepared =
1813-
super::prepare_server_config(&mut args, &matches).expect("server config is prepared");
1890+
let prepared = super::prepare_server_config(
1891+
&mut args,
1892+
&matches,
1893+
&crate::install_default_compute_drivers(),
1894+
)
1895+
.expect("server config is prepared");
18141896

18151897
assert_eq!(prepared.config.compute_drivers, vec!["podman".to_string()]);
18161898
assert_eq!(

0 commit comments

Comments
 (0)