Skip to content

Commit fd09c75

Browse files
committed
fix(core): pin supervisor image tag to gateway version for all drivers
The Podman and Kubernetes drivers defaulted the supervisor image to `:latest` via DEFAULT_SUPERVISOR_IMAGE, while the Docker driver already resolved a version-pinned tag. Extract the tag resolution logic into openshell-core so all three drivers use the same OPENSHELL_IMAGE_TAG > IMAGE_TAG > CARGO_PKG_VERSION priority chain. Closes #2068 Signed-off-by: Florent Benoit <fbenoit@redhat.com>
1 parent 45614a3 commit fd09c75

9 files changed

Lines changed: 119 additions & 62 deletions

File tree

crates/openshell-core/src/config.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,50 @@ pub const DEFAULT_DOCKER_NETWORK_NAME: &str = "openshell-docker";
3737
/// Default domain used for browser-facing sandbox service URLs.
3838
pub const DEFAULT_SERVICE_ROUTING_DOMAIN: &str = "openshell.localhost";
3939

40-
/// Default OCI image for the openshell-sandbox supervisor binary.
41-
pub const DEFAULT_SUPERVISOR_IMAGE: &str = "ghcr.io/nvidia/openshell/supervisor:latest";
40+
/// Default OCI repository for the supervisor image (no tag).
41+
pub const DEFAULT_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/supervisor";
42+
43+
/// Return the default supervisor image reference with a version-pinned tag.
44+
#[must_use]
45+
pub fn default_supervisor_image() -> String {
46+
format!(
47+
"{DEFAULT_SUPERVISOR_IMAGE_REPO}:{}",
48+
default_supervisor_image_tag()
49+
)
50+
}
51+
52+
fn default_supervisor_image_tag() -> String {
53+
resolve_supervisor_image_tag(
54+
option_env!("OPENSHELL_IMAGE_TAG"),
55+
option_env!("IMAGE_TAG"),
56+
env!("CARGO_PKG_VERSION"),
57+
)
58+
}
59+
60+
/// Resolve the supervisor image tag from build-time inputs.
61+
///
62+
/// Priority: `OPENSHELL_IMAGE_TAG` > `IMAGE_TAG` > `CARGO_PKG_VERSION`.
63+
/// Falls back to `"dev"` when the Cargo version is empty or `"0.0.0"`.
64+
/// Replaces `+` with `-` for OCI tag compatibility.
65+
#[must_use]
66+
pub fn resolve_supervisor_image_tag(
67+
openshell_image_tag: Option<&str>,
68+
image_tag: Option<&str>,
69+
cargo_pkg_version: &str,
70+
) -> String {
71+
let tag = openshell_image_tag
72+
.filter(|tag| !tag.is_empty())
73+
.or_else(|| image_tag.filter(|tag| !tag.is_empty()))
74+
.unwrap_or_else(|| {
75+
if cargo_pkg_version.is_empty() || cargo_pkg_version == "0.0.0" {
76+
"dev"
77+
} else {
78+
cargo_pkg_version
79+
}
80+
});
81+
82+
tag.replace('+', "-")
83+
}
4284

4385
/// CDI device identifier for requesting all NVIDIA GPUs.
4486
pub const CDI_GPU_DEVICE_ALL: &str = "nvidia.com/gpu=all";
@@ -1064,4 +1106,48 @@ mod tests {
10641106
}
10651107
}
10661108
}
1109+
1110+
#[test]
1111+
fn supervisor_image_tag_prefers_explicit_build_tags() {
1112+
use super::resolve_supervisor_image_tag;
1113+
assert_eq!(
1114+
resolve_supervisor_image_tag(Some("1.2.3"), Some("sha"), "0.0.0"),
1115+
"1.2.3",
1116+
);
1117+
assert_eq!(
1118+
resolve_supervisor_image_tag(None, Some("sha"), "0.0.0"),
1119+
"sha",
1120+
);
1121+
assert_eq!(resolve_supervisor_image_tag(None, None, "1.2.3"), "1.2.3",);
1122+
assert_eq!(
1123+
resolve_supervisor_image_tag(Some(""), Some(""), "0.0.0"),
1124+
"dev",
1125+
);
1126+
assert_eq!(
1127+
resolve_supervisor_image_tag(Some("latest"), None, "1.2.3"),
1128+
"latest",
1129+
);
1130+
}
1131+
1132+
#[test]
1133+
fn supervisor_image_tag_sanitizes_build_metadata_for_oci() {
1134+
use super::resolve_supervisor_image_tag;
1135+
assert_eq!(
1136+
resolve_supervisor_image_tag(None, None, "0.0.37-dev.156+g1d3b741ee"),
1137+
"0.0.37-dev.156-g1d3b741ee",
1138+
);
1139+
assert_eq!(
1140+
resolve_supervisor_image_tag(Some("0.0.37-dev.156+g1d3b741ee"), None, "0.0.0"),
1141+
"0.0.37-dev.156-g1d3b741ee",
1142+
);
1143+
}
1144+
1145+
#[test]
1146+
fn default_supervisor_image_is_version_pinned() {
1147+
use super::default_supervisor_image;
1148+
let image = default_supervisor_image();
1149+
assert!(image.starts_with("ghcr.io/nvidia/openshell/supervisor:"));
1150+
let tag = image.rsplit_once(':').unwrap().1;
1151+
assert!(!tag.is_empty());
1152+
}
10671153
}

crates/openshell-driver-docker/src/lib.rs

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -78,55 +78,24 @@ const HOST_OPENSHELL_INTERNAL: &str = "host.openshell.internal";
7878
const HOST_DOCKER_INTERNAL: &str = "host.docker.internal";
7979
const DOCKER_NETWORK_DRIVER: &str = "bridge";
8080

81-
/// Default image holding the Linux `openshell-sandbox` binary. The gateway
82-
/// pulls this image and extracts the binary to a host-side cache when no
83-
/// explicit `supervisor_bin`, configured `supervisor_image`, sibling binary,
84-
/// or local build is available.
85-
const DEFAULT_DOCKER_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/supervisor";
86-
8781
/// Return the default `ghcr.io/nvidia/openshell/supervisor:<tag>` reference
8882
/// used when no supervisor binary override is provided.
8983
pub fn default_docker_supervisor_image() -> String {
9084
format!(
91-
"{DEFAULT_DOCKER_SUPERVISOR_IMAGE_REPO}:{}",
85+
"{}:{}",
86+
openshell_core::config::DEFAULT_SUPERVISOR_IMAGE_REPO,
9287
default_docker_supervisor_image_tag()
9388
)
9489
}
9590

96-
/// Image tag baked in at compile time to pair the gateway with a matching
97-
/// supervisor image.
98-
///
99-
/// Build pipelines pass `OPENSHELL_IMAGE_TAG` explicitly. The `IMAGE_TAG`
100-
/// fallback covers image build wrappers that already tag the gateway and
101-
/// supervisor together. Standalone release binaries also patch the Cargo
102-
/// package version, so use it when it has been set to a real release value.
10391
fn default_docker_supervisor_image_tag() -> String {
104-
resolve_default_docker_supervisor_image_tag(
92+
openshell_core::config::resolve_supervisor_image_tag(
10593
option_env!("OPENSHELL_IMAGE_TAG"),
10694
option_env!("IMAGE_TAG"),
10795
env!("CARGO_PKG_VERSION"),
10896
)
10997
}
11098

111-
fn resolve_default_docker_supervisor_image_tag(
112-
openshell_image_tag: Option<&'static str>,
113-
image_tag: Option<&'static str>,
114-
cargo_pkg_version: &'static str,
115-
) -> String {
116-
let tag = openshell_image_tag
117-
.filter(|tag| !tag.is_empty())
118-
.or_else(|| image_tag.filter(|tag| !tag.is_empty()))
119-
.unwrap_or_else(|| {
120-
if cargo_pkg_version.is_empty() || cargo_pkg_version == "0.0.0" {
121-
"dev"
122-
} else {
123-
cargo_pkg_version
124-
}
125-
});
126-
127-
tag.replace('+', "-")
128-
}
129-
13099
/// Queried by the Docker driver to decide when a sandbox's supervisor
131100
/// relay is live. Implementations return `true` once a sandbox has an
132101
/// active `ConnectSupervisor` session registered.

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,36 +1897,31 @@ fn configured_supervisor_image_takes_precedence_over_local_binaries() {
18971897

18981898
#[test]
18991899
fn docker_supervisor_image_tag_prefers_explicit_build_tags() {
1900+
use openshell_core::config::resolve_supervisor_image_tag;
19001901
assert_eq!(
1901-
resolve_default_docker_supervisor_image_tag(Some("1.2.3"), Some("sha"), "0.0.0"),
1902+
resolve_supervisor_image_tag(Some("1.2.3"), Some("sha"), "0.0.0"),
19021903
"1.2.3",
19031904
);
19041905
assert_eq!(
1905-
resolve_default_docker_supervisor_image_tag(None, Some("sha"), "0.0.0"),
1906+
resolve_supervisor_image_tag(None, Some("sha"), "0.0.0"),
19061907
"sha",
19071908
);
1909+
assert_eq!(resolve_supervisor_image_tag(None, None, "1.2.3"), "1.2.3",);
19081910
assert_eq!(
1909-
resolve_default_docker_supervisor_image_tag(None, None, "1.2.3"),
1910-
"1.2.3",
1911-
);
1912-
assert_eq!(
1913-
resolve_default_docker_supervisor_image_tag(Some(""), Some(""), "0.0.0"),
1911+
resolve_supervisor_image_tag(Some(""), Some(""), "0.0.0"),
19141912
"dev",
19151913
);
19161914
}
19171915

19181916
#[test]
19191917
fn docker_supervisor_image_tag_sanitizes_build_metadata_for_docker() {
1918+
use openshell_core::config::resolve_supervisor_image_tag;
19201919
assert_eq!(
1921-
resolve_default_docker_supervisor_image_tag(None, None, "0.0.37-dev.156+g1d3b741ee"),
1920+
resolve_supervisor_image_tag(None, None, "0.0.37-dev.156+g1d3b741ee"),
19221921
"0.0.37-dev.156-g1d3b741ee",
19231922
);
19241923
assert_eq!(
1925-
resolve_default_docker_supervisor_image_tag(
1926-
Some("0.0.37-dev.156+g1d3b741ee"),
1927-
None,
1928-
"0.0.0",
1929-
),
1924+
resolve_supervisor_image_tag(Some("0.0.37-dev.156+g1d3b741ee"), None, "0.0.0",),
19301925
"0.0.37-dev.156-g1d3b741ee",
19311926
);
19321927
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
use openshell_core::config::DEFAULT_SUPERVISOR_IMAGE;
4+
use openshell_core::config;
55
use serde::{Deserialize, Deserializer, Serialize};
66
use std::path::Path;
77
use std::str::FromStr;
@@ -263,7 +263,7 @@ impl Default for KubernetesComputeConfig {
263263
// is Podman vocabulary and is not a valid Kubernetes value.
264264
image_pull_policy: String::new(),
265265
image_pull_secrets: Vec::new(),
266-
supervisor_image: DEFAULT_SUPERVISOR_IMAGE.to_string(),
266+
supervisor_image: config::default_supervisor_image(),
267267
supervisor_image_pull_policy: String::new(),
268268
supervisor_sideload_method: SupervisorSideloadMethod::default(),
269269
supervisor_topology: SupervisorTopology::default(),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async fn main() -> Result<()> {
121121
image_pull_secrets: args.sandbox_image_pull_secrets,
122122
supervisor_image: args
123123
.supervisor_image
124-
.unwrap_or_else(|| openshell_core::config::DEFAULT_SUPERVISOR_IMAGE.to_string()),
124+
.unwrap_or_else(openshell_core::config::default_supervisor_image),
125125
supervisor_image_pull_policy: args.supervisor_image_pull_policy.unwrap_or_default(),
126126
supervisor_sideload_method: args.supervisor_sideload_method,
127127
supervisor_topology: args.supervisor_topology,

crates/openshell-driver-podman/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
use openshell_core::config::{DEFAULT_STOP_TIMEOUT_SECS, DEFAULT_SUPERVISOR_IMAGE};
4+
use openshell_core::config::DEFAULT_STOP_TIMEOUT_SECS;
55
use std::net::IpAddr;
66
use std::path::PathBuf;
77
use std::str::FromStr;
@@ -255,7 +255,7 @@ impl Default for PodmanComputeConfig {
255255
network_name: DEFAULT_NETWORK_NAME.to_string(),
256256
host_gateway_ip: Self::default_host_gateway_ip(),
257257
stop_timeout_secs: DEFAULT_STOP_TIMEOUT_SECS,
258-
supervisor_image: DEFAULT_SUPERVISOR_IMAGE.to_string(),
258+
supervisor_image: openshell_core::config::default_supervisor_image(),
259259
guest_tls_ca: None,
260260
guest_tls_cert: None,
261261
guest_tls_key: None,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,7 +1798,7 @@ mod tests {
17981798
let vol = &image_volumes[0];
17991799
assert_eq!(
18001800
vol["source"].as_str(),
1801-
Some("ghcr.io/nvidia/openshell/supervisor:latest"),
1801+
Some(openshell_core::config::default_supervisor_image().as_str()),
18021802
"image volume source should be the supervisor image"
18031803
);
18041804
assert_eq!(
@@ -1884,8 +1884,9 @@ mod tests {
18841884
let image_volumes = spec["image_volumes"]
18851885
.as_array()
18861886
.expect("image_volumes should be an array");
1887+
let expected_supervisor = openshell_core::config::default_supervisor_image();
18871888
assert!(image_volumes.iter().any(|volume| {
1888-
volume["source"].as_str() == Some("ghcr.io/nvidia/openshell/supervisor:latest")
1889+
volume["source"].as_str() == Some(expected_supervisor.as_str())
18891890
&& volume["destination"].as_str() == Some("/opt/openshell/bin")
18901891
}));
18911892
assert!(image_volumes.iter().any(|volume| {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ struct Args {
9090

9191
/// OCI image containing the openshell-sandbox supervisor binary.
9292
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
93-
supervisor_image: String,
93+
supervisor_image: Option<String>,
9494

9595
/// Host path to the CA certificate for sandbox mTLS.
9696
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CA")]
@@ -130,7 +130,9 @@ async fn main() -> Result<()> {
130130
sandbox_ssh_socket_path: args.sandbox_ssh_socket_path,
131131
network_name: args.network_name,
132132
stop_timeout_secs: args.stop_timeout,
133-
supervisor_image: args.supervisor_image,
133+
supervisor_image: args
134+
.supervisor_image
135+
.unwrap_or_else(openshell_core::config::default_supervisor_image),
134136
guest_tls_ca: args.podman_tls_ca,
135137
guest_tls_cert: args.podman_tls_cert,
136138
guest_tls_key: args.podman_tls_key,

docs/reference/gateway-config.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ disable_tls = false
8888
# Shared driver defaults. These inherit into [openshell.drivers.<name>] tables
8989
# when the driver-specific table does not override them.
9090
default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
91-
supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest"
91+
# Defaults to the gateway version; override to pin a specific build.
92+
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
9293
client_tls_secret_name = "openshell-client-tls"
9394
service_account_name = "openshell-sandbox"
9495
host_gateway_ip = "10.0.0.1"
@@ -172,7 +173,8 @@ service_account_name = "openshell-sandbox"
172173
default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
173174
image_pull_policy = "IfNotPresent"
174175
image_pull_secrets = ["regcred"]
175-
supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest"
176+
# Defaults to the gateway version; override to pin a specific build.
177+
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
176178
supervisor_image_pull_policy = "IfNotPresent"
177179
# Use the image volume on Kubernetes >= 1.35 (GA in 1.36); switch to "init-container"
178180
# on older clusters or where the ImageVolume feature gate is off.
@@ -222,7 +224,8 @@ grpc_endpoint = "https://host.openshell.internal:17670"
222224
# Skip the image-pull-and-extract step by pointing at a locally built binary.
223225
supervisor_bin = "/usr/local/libexec/openshell/openshell-sandbox"
224226
# When supervisor_bin is omitted, Docker extracts /openshell-sandbox from this image.
225-
supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest"
227+
# Defaults to the gateway version; override to pin a specific build.
228+
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
226229
guest_tls_ca = "/etc/openshell/certs/ca.pem"
227230
guest_tls_cert = "/etc/openshell/certs/client.pem"
228231
guest_tls_key = "/etc/openshell/certs/client-key.pem"
@@ -264,7 +267,8 @@ network_name = "openshell"
264267
# host_gateway_ip = "192.168.127.254"
265268
sandbox_ssh_socket_path = "/run/openshell/ssh.sock"
266269
stop_timeout_secs = 10
267-
supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest"
270+
# Defaults to the gateway version; override to pin a specific build.
271+
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
268272
guest_tls_ca = "/etc/openshell/certs/ca.pem"
269273
guest_tls_cert = "/etc/openshell/certs/client.pem"
270274
guest_tls_key = "/etc/openshell/certs/client-key.pem"

0 commit comments

Comments
 (0)