Skip to content

Commit e9271cb

Browse files
authored
feat(gateway): support runtime image env overrides (#3504)
Closes #3502 Resolve trusted OCI runtime images with driver TOML, process environment, and compiled-default precedence across Docker, Podman, and Kubernetes. Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent e7fdd6b commit e9271cb

20 files changed

Lines changed: 312 additions & 52 deletions

File tree

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

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,24 +89,48 @@ pub const DEFAULT_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/superv
8989
/// Default OCI repository for the sandbox runtime image (no tag).
9090
pub const DEFAULT_SANDBOX_RUNTIME_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/sandbox";
9191

92-
/// Return the default sandbox runtime image reference with a version-pinned tag.
93-
#[must_use]
94-
pub fn default_sandbox_runtime_image() -> String {
92+
/// Process-level default for the trusted sandbox runtime image.
93+
pub const SANDBOX_RUNTIME_IMAGE_ENV: &str = "OPENSHELL_SANDBOX_RUNTIME_IMAGE";
94+
95+
/// Process-level default for the trusted supervisor image.
96+
pub const SUPERVISOR_IMAGE_ENV: &str = "OPENSHELL_SUPERVISOR_IMAGE";
97+
98+
fn compiled_sandbox_runtime_image() -> String {
9599
format!(
96100
"{DEFAULT_SANDBOX_RUNTIME_IMAGE_REPO}:{}",
97101
default_supervisor_image_tag()
98102
)
99103
}
100104

101-
/// Return the default supervisor image reference with a version-pinned tag.
102-
#[must_use]
103-
pub fn default_supervisor_image() -> String {
105+
fn compiled_supervisor_image() -> String {
104106
format!(
105107
"{DEFAULT_SUPERVISOR_IMAGE_REPO}:{}",
106108
default_supervisor_image_tag()
107109
)
108110
}
109111

112+
fn runtime_image_default(environment_value: Option<String>, compiled_default: String) -> String {
113+
environment_value.unwrap_or(compiled_default)
114+
}
115+
116+
/// Return the process-configured sandbox runtime image, or the compiled default.
117+
#[must_use]
118+
pub fn default_sandbox_runtime_image() -> String {
119+
runtime_image_default(
120+
std::env::var(SANDBOX_RUNTIME_IMAGE_ENV).ok(),
121+
compiled_sandbox_runtime_image(),
122+
)
123+
}
124+
125+
/// Return the process-configured supervisor image, or the compiled default.
126+
#[must_use]
127+
pub fn default_supervisor_image() -> String {
128+
runtime_image_default(
129+
std::env::var(SUPERVISOR_IMAGE_ENV).ok(),
130+
compiled_supervisor_image(),
131+
)
132+
}
133+
110134
fn default_supervisor_image_tag() -> String {
111135
resolve_supervisor_image_tag(&[
112136
option_env!("OPENSHELL_IMAGE_TAG").unwrap_or(""),
@@ -1644,15 +1668,30 @@ mod tests {
16441668

16451669
#[test]
16461670
fn default_supervisor_image_is_version_pinned() {
1647-
use super::{default_sandbox_runtime_image, default_supervisor_image};
1648-
let image = default_supervisor_image();
1671+
use super::{compiled_sandbox_runtime_image, compiled_supervisor_image};
1672+
let image = compiled_supervisor_image();
16491673
assert!(image.starts_with("ghcr.io/nvidia/openshell/supervisor:"));
16501674
let tag = image.rsplit_once(':').unwrap().1;
16511675
assert!(!tag.is_empty());
16521676

1653-
let sandbox_image = default_sandbox_runtime_image();
1677+
let sandbox_image = compiled_sandbox_runtime_image();
16541678
assert!(sandbox_image.starts_with("ghcr.io/nvidia/openshell/sandbox:"));
16551679
let sandbox_tag = sandbox_image.rsplit_once(':').unwrap().1;
16561680
assert!(!sandbox_tag.is_empty());
16571681
}
1682+
1683+
#[test]
1684+
fn runtime_image_environment_value_replaces_compiled_default() {
1685+
use super::runtime_image_default;
1686+
1687+
let digest = format!("registry.example.com/sandbox@sha256:{}", "a".repeat(64));
1688+
assert_eq!(
1689+
runtime_image_default(Some(digest.clone()), "compiled:default".to_string()),
1690+
digest
1691+
);
1692+
assert_eq!(
1693+
runtime_image_default(None, "compiled:default".to_string()),
1694+
"compiled:default"
1695+
);
1696+
}
16581697
}

‎crates/openshell-driver-docker/src/main.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ struct Args {
2525
#[arg(long, env = "OPENSHELL_DOCKER_DRIVER_CONFIG")]
2626
config: PathBuf,
2727

28+
/// OCI image containing the `openshell-sandbox` runtime binary.
29+
#[arg(long, env = openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)]
30+
sandbox_runtime_image: Option<String>,
31+
32+
/// OCI image containing the `openshell-supervisor` control binary.
33+
#[arg(long, env = openshell_core::config::SUPERVISOR_IMAGE_ENV)]
34+
supervisor_image: Option<String>,
35+
2836
/// Gateway listener address used to derive the supervisor endpoint.
2937
#[arg(
3038
long,
@@ -63,6 +71,12 @@ async fn main() -> Result<()> {
6371
docker_config.allow_driver_config = policy.allow_driver_config;
6472
docker_config.resource_admission = policy.resource_admission;
6573
}
74+
if let Some(image) = args.sandbox_runtime_image {
75+
docker_config.sandbox_runtime_image = Some(image);
76+
}
77+
if let Some(image) = args.supervisor_image {
78+
docker_config.supervisor_image = Some(image);
79+
}
6680
let driver = DockerComputeDriver::new(args.gateway_bind, &args.log_level, &docker_config)
6781
.await
6882
.into_diagnostic()?;

‎crates/openshell-driver-docker/src/tests.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2503,6 +2503,23 @@ fn validate_sandbox_rejects_unknown_driver_config_fields() {
25032503
assert!(err.message().contains("unknown field"));
25042504
}
25052505

2506+
#[test]
2507+
fn sandbox_driver_config_rejects_trusted_runtime_image_overrides() {
2508+
for field in ["sandbox_runtime_image", "supervisor_image"] {
2509+
let template = DriverSandboxTemplate {
2510+
driver_config: Some(json_struct(serde_json::json!({
2511+
(field): "registry.example.com/openshell/runtime:untrusted"
2512+
}))),
2513+
..Default::default()
2514+
};
2515+
2516+
let error = DockerSandboxDriverConfig::from_template(&template)
2517+
.expect_err("sandbox requests must not select trusted runtime images");
2518+
assert!(error.contains("unknown field"), "{error}");
2519+
assert!(error.contains(field), "{error}");
2520+
}
2521+
}
2522+
25062523
#[test]
25072524
fn validate_sandbox_accepts_gpu_count_request_shape() {
25082525
let mut config = runtime_config();

‎crates/openshell-driver-kubernetes/src/driver.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8865,6 +8865,23 @@ mod tests {
88658865
assert!(err.contains("unknown field"));
88668866
}
88678867

8868+
#[test]
8869+
fn sandbox_driver_config_rejects_trusted_runtime_image_overrides() {
8870+
for field in ["sandbox_runtime_image", "supervisor_image"] {
8871+
let template = SandboxTemplate {
8872+
driver_config: Some(json_struct(serde_json::json!({
8873+
(field): "registry.example.com/openshell/runtime:untrusted"
8874+
}))),
8875+
..Default::default()
8876+
};
8877+
8878+
let error = KubernetesSandboxDriverConfig::from_template(&template)
8879+
.expect_err("sandbox requests must not select trusted runtime images");
8880+
assert!(error.contains("unknown field"), "{error}");
8881+
assert!(error.contains(field), "{error}");
8882+
}
8883+
}
8884+
88688885
#[test]
88698886
fn driver_config_for_spec_rejects_unknown_fields() {
88708887
let sandbox = Sandbox {

‎crates/openshell-driver-kubernetes/src/main.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,13 @@ struct Args {
116116
#[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")]
117117
host_gateway_ip: Option<String>,
118118

119-
#[arg(long, env = "OPENSHELL_SANDBOX_RUNTIME_IMAGE")]
119+
#[arg(long, env = openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)]
120120
sandbox_runtime_image: Option<String>,
121121

122122
#[arg(long, env = "OPENSHELL_SANDBOX_RUNTIME_IMAGE_PULL_POLICY")]
123123
sandbox_runtime_image_pull_policy: Option<KubernetesImagePullPolicy>,
124124

125-
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
125+
#[arg(long, env = openshell_core::config::SUPERVISOR_IMAGE_ENV)]
126126
supervisor_image: Option<String>,
127127

128128
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY")]

‎crates/openshell-driver-podman/src/container.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,25 @@ mod tests {
23132313
assert!(err.to_string().contains("unknown field"));
23142314
}
23152315

2316+
#[test]
2317+
fn sandbox_driver_config_rejects_trusted_runtime_image_overrides() {
2318+
use openshell_core::proto::compute::v1::DriverSandboxTemplate;
2319+
2320+
for field in ["sandbox_runtime_image", "supervisor_image"] {
2321+
let template = DriverSandboxTemplate {
2322+
driver_config: Some(json_struct(serde_json::json!({
2323+
(field): "registry.example.com/openshell/runtime:untrusted"
2324+
}))),
2325+
..Default::default()
2326+
};
2327+
2328+
let error = PodmanSandboxDriverConfig::from_template(&template)
2329+
.expect_err("sandbox requests must not select trusted runtime images");
2330+
assert!(error.to_string().contains("unknown field"), "{error}");
2331+
assert!(error.to_string().contains(field), "{error}");
2332+
}
2333+
}
2334+
23162335
#[test]
23172336
fn container_spec_defaults_drop_capabilities_and_keep_runtime_seccomp() {
23182337
let sandbox = test_sandbox("test-id", "test-name");

‎crates/openshell-driver-podman/src/main.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,11 @@ struct Args {
110110
health_check_interval_secs: Option<NonZeroU64>,
111111

112112
/// OCI image containing the `openshell-sandbox` runtime binary.
113-
#[arg(long, env = "OPENSHELL_SANDBOX_RUNTIME_IMAGE")]
113+
#[arg(long, env = openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)]
114114
sandbox_runtime_image: Option<String>,
115115

116116
/// OCI image containing the `openshell-supervisor` control binary.
117-
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
117+
#[arg(long, env = openshell_core::config::SUPERVISOR_IMAGE_ENV)]
118118
supervisor_image: Option<String>,
119119

120120
/// Host path to the CA certificate for supervisor-to-gateway TLS.

‎crates/openshell-gateway/src/lib.rs‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ use openshell_core::telemetry::TelemetryComputeDriver;
4242
use openshell_server::ComputeDriverRegistration;
4343
use openshell_server::ComputeDriverRegistry;
4444

45+
#[cfg(all(
46+
not(target_os = "windows"),
47+
any(
48+
feature = "compute-driver-docker",
49+
feature = "compute-driver-kubernetes",
50+
feature = "compute-driver-podman"
51+
)
52+
))]
53+
fn prefer_environment<T>(configured: T, environment: Option<T>) -> T {
54+
environment.unwrap_or(configured)
55+
}
56+
4557
/// Install every first-party compute driver linked into the standard gateway.
4658
#[must_use]
4759
pub fn install_default_compute_drivers() -> ComputeDriverRegistry {
@@ -272,6 +284,14 @@ fn kubernetes_config(
272284
) -> openshell_core::Result<openshell_driver_kubernetes::KubernetesComputeConfig> {
273285
let mut config: openshell_driver_kubernetes::KubernetesComputeConfig =
274286
context.driver_config()?;
287+
config.sandbox_runtime_image = prefer_environment(
288+
config.sandbox_runtime_image,
289+
std::env::var(openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV).ok(),
290+
);
291+
config.supervisor_image = prefer_environment(
292+
config.supervisor_image,
293+
std::env::var(openshell_core::config::SUPERVISOR_IMAGE_ENV).ok(),
294+
);
275295
if let Ok(size) = std::env::var("OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE") {
276296
config.workspace_default_storage_size = size;
277297
}
@@ -296,15 +316,15 @@ impl openshell_server::ComputeDriverFactory for DockerFactory {
296316
&self,
297317
context: openshell_server::ComputeDriverConfigContext<'_>,
298318
) -> openshell_core::Result<()> {
299-
let config: openshell_driver_docker::DockerComputeConfig = context.driver_config()?;
319+
let config = docker_config(context)?;
300320
config.validate_configuration(context.gateway_bind_address())
301321
}
302322

303323
async fn build(
304324
&self,
305325
context: openshell_server::ComputeDriverBuildContext<'_>,
306326
) -> openshell_core::Result<openshell_server::ComputeDriverInstance> {
307-
let mut config: openshell_driver_docker::DockerComputeConfig = context.driver_config()?;
327+
let mut config = docker_config(context.config_context())?;
308328
require_guest_tls_for_local_driver(&context, "docker")?;
309329
apply_guest_tls(&mut config.guest_tls_ca, context.guest_tls_ca());
310330
let driver = openshell_driver_docker::DockerComputeDriver::new(
@@ -321,6 +341,26 @@ impl openshell_server::ComputeDriverFactory for DockerFactory {
321341
}
322342
}
323343

344+
#[cfg(all(not(target_os = "windows"), feature = "compute-driver-docker"))]
345+
fn docker_config(
346+
context: openshell_server::ComputeDriverConfigContext<'_>,
347+
) -> openshell_core::Result<openshell_driver_docker::DockerComputeConfig> {
348+
let mut config: openshell_driver_docker::DockerComputeConfig = context.driver_config()?;
349+
config.sandbox_runtime_image = prefer_environment(
350+
config.sandbox_runtime_image,
351+
std::env::var(openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV)
352+
.ok()
353+
.map(Some),
354+
);
355+
config.supervisor_image = prefer_environment(
356+
config.supervisor_image,
357+
std::env::var(openshell_core::config::SUPERVISOR_IMAGE_ENV)
358+
.ok()
359+
.map(Some),
360+
);
361+
Ok(config)
362+
}
363+
324364
#[cfg(all(not(target_os = "windows"), feature = "compute-driver-podman"))]
325365
#[derive(Clone, Copy)]
326366
struct PodmanFactory;
@@ -364,6 +404,14 @@ fn podman_config(
364404
) -> openshell_core::Result<openshell_driver_podman::PodmanComputeConfig> {
365405
let mut config: openshell_driver_podman::PodmanComputeConfig = context.driver_config()?;
366406
config.gateway_port = context.gateway_port();
407+
config.sandbox_runtime_image = prefer_environment(
408+
config.sandbox_runtime_image,
409+
std::env::var(openshell_core::config::SANDBOX_RUNTIME_IMAGE_ENV).ok(),
410+
);
411+
config.supervisor_image = prefer_environment(
412+
config.supervisor_image,
413+
std::env::var(openshell_core::config::SUPERVISOR_IMAGE_ENV).ok(),
414+
);
367415
if let Ok(path) = std::env::var("OPENSHELL_PODMAN_SOCKET") {
368416
config.socket_path = Some(path.into());
369417
}
@@ -574,6 +622,23 @@ mod windows_tests {
574622
mod tests {
575623
use super::*;
576624

625+
#[test]
626+
#[cfg(all(
627+
not(target_os = "windows"),
628+
any(
629+
feature = "compute-driver-docker",
630+
feature = "compute-driver-kubernetes",
631+
feature = "compute-driver-podman"
632+
)
633+
))]
634+
fn runtime_image_environment_overrides_configured_value() {
635+
assert_eq!(
636+
prefer_environment("driver-toml", Some("process-environment")),
637+
"process-environment"
638+
);
639+
assert_eq!(prefer_environment("driver-toml", None), "driver-toml");
640+
}
641+
577642
#[test]
578643
fn default_registry_contains_exactly_the_enabled_compute_drivers() {
579644
let expected: Vec<&str> = vec![

‎deploy/man/openshell-gateway.8.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ need a custom bundle location.
175175
The gateway then starts from built-in defaults and reads
176176
*~/.config/openshell/gateway.toml* when that file exists.
177177

178+
The user service also reads *~/.config/openshell/gateway.env* for both config
179+
preflight and startup. **OPENSHELL_SANDBOX_RUNTIME_IMAGE** and
180+
**OPENSHELL_SUPERVISOR_IMAGE** accept complete tagged or digest-pinned OCI
181+
references for the built-in Docker, Podman, and Kubernetes drivers. These
182+
variables take precedence over explicit image fields in the selected driver's
183+
TOML table, which take precedence over compiled release defaults.
184+
These are trusted gateway inputs and cannot be supplied by sandbox requests.
185+
Do not embed registry credentials in image references.
186+
178187
To persist the service across logouts:
179188

180189
sudo loginctl enable-linger $USER

‎deploy/rpm/CONFIGURATION.md‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ editing the TOML file, add them to `~/.config/openshell/gateway.env`:
5151
OPENSHELL_BIND_ADDRESS=192.168.1.10
5252
```
5353

54+
To select exact trusted runtime artifacts across the built-in Docker, Podman,
55+
or Kubernetes driver, set complete tagged or digest-pinned references:
56+
57+
```shell
58+
OPENSHELL_SANDBOX_RUNTIME_IMAGE=registry.example.com/openshell/sandbox@sha256:<digest>
59+
OPENSHELL_SUPERVISOR_IMAGE=registry.example.com/openshell/supervisor@sha256:<digest>
60+
```
61+
62+
`OPENSHELL_SANDBOX_RUNTIME_IMAGE` and `OPENSHELL_SUPERVISOR_IMAGE` take
63+
precedence over explicit image fields in the selected driver's TOML table; the
64+
driver TOML takes precedence over the compiled release default. Preflight and
65+
startup use the same resolution. These values are trusted operator inputs, not
66+
sandbox request fields. Keep registry credentials in Podman's credential store
67+
rather than embedding them in image references.
68+
5469
To override the path to the TOML config file entirely:
5570

5671
```shell
@@ -216,8 +231,8 @@ overrides that persist across package upgrades.
216231
| `bind_address` | `127.0.0.1:17670` (gateway default) | Address for the primary gRPC/HTTP API listener. |
217232
| `compute_driver` | `"podman"` (RPM default) | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. The RPM default pins to Podman; legacy `compute_drivers` lists are rejected. |
218233
| `[openshell.drivers.podman].default_image` | `nvcr.io/nvidia/base/ubuntu:24.04` | Default sandbox image. |
219-
| `[openshell.drivers.podman].sandbox_runtime_image` | `ghcr.io/nvidia/openshell/sandbox:latest` | Static musl sandbox runtime image mounted into Podman workloads. |
220-
| `[openshell.drivers.podman].supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Dynamic glibc supervisor image used outside the workload. |
234+
| `[openshell.drivers.podman].sandbox_runtime_image` | `ghcr.io/nvidia/openshell/sandbox:latest` | Trusted sandbox runtime image. `OPENSHELL_SANDBOX_RUNTIME_IMAGE` overrides this field. |
235+
| `[openshell.drivers.podman].supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Trusted supervisor image. `OPENSHELL_SUPERVISOR_IMAGE` overrides this field. |
221236
| `[openshell.gateway].guest_tls_ca` | auto-generated path | Gateway CA injected into the selected local driver for supervisor-to-gateway TLS. Sandbox identity uses a bearer token. |
222237
| `[openshell.gateway.tls]` paths | auto-generated paths | Server TLS certificate, key, and client CA. |
223238
| `disable_tls` | unset | Set to `true` to disable TLS. |

0 commit comments

Comments
 (0)