Skip to content

Commit e641648

Browse files
authored
feat(sandbox): add configurable imagePullPolicy for sandbox pods (#256)
Add OPENSHELL_SANDBOX_IMAGE_PULL_POLICY env/CLI flag that propagates through Config -> SandboxClient -> pod spec generation. When set, the policy is applied to both inline and pod-template sandbox containers. Helm chart exposes server.sandboxImagePullPolicy (default empty = K8s default behavior). cluster-deploy-fast sets it to Always so dev clusters always pull fresh sandbox images.
1 parent ec44ca9 commit e641648

File tree

7 files changed

+61
-0
lines changed

7 files changed

+61
-0
lines changed

crates/navigator-core/src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ pub struct Config {
3232
#[serde(default)]
3333
pub sandbox_image: String,
3434

35+
/// Kubernetes `imagePullPolicy` for sandbox pods (e.g. `Always`,
36+
/// `IfNotPresent`, `Never`). Defaults to empty, which lets Kubernetes
37+
/// apply its own default (`:latest` → `Always`, anything else →
38+
/// `IfNotPresent`).
39+
#[serde(default)]
40+
pub sandbox_image_pull_policy: String,
41+
3542
/// gRPC endpoint for sandboxes to connect back to OpenShell.
3643
/// Used by sandbox pods to fetch their policy at startup.
3744
#[serde(default)]
@@ -108,6 +115,7 @@ impl Config {
108115
database_url: String::new(),
109116
sandbox_namespace: default_sandbox_namespace(),
110117
sandbox_image: String::new(),
118+
sandbox_image_pull_policy: String::new(),
111119
grpc_endpoint: String::new(),
112120
ssh_gateway_host: default_ssh_gateway_host(),
113121
ssh_gateway_port: default_ssh_gateway_port(),
@@ -155,6 +163,13 @@ impl Config {
155163
self
156164
}
157165

166+
/// Create a new configuration with a sandbox image pull policy.
167+
#[must_use]
168+
pub fn with_sandbox_image_pull_policy(mut self, policy: impl Into<String>) -> Self {
169+
self.sandbox_image_pull_policy = policy.into();
170+
self
171+
}
172+
158173
/// Create a new configuration with a gRPC endpoint for sandbox callback.
159174
#[must_use]
160175
pub fn with_grpc_endpoint(mut self, endpoint: impl Into<String>) -> Self {

crates/navigator-server/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ pub async fn run_server(config: Config, tracing_log_bus: TracingLogBus) -> Resul
113113
let sandbox_client = SandboxClient::new(
114114
config.sandbox_namespace.clone(),
115115
config.sandbox_image.clone(),
116+
config.sandbox_image_pull_policy.clone(),
116117
config.grpc_endpoint.clone(),
117118
format!("0.0.0.0:{}", config.sandbox_ssh_port),
118119
config.ssh_handshake_secret.clone(),

crates/navigator-server/src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ struct Args {
4949
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
5050
sandbox_image: Option<String>,
5151

52+
/// Kubernetes imagePullPolicy for sandbox pods (Always, IfNotPresent, Never).
53+
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY")]
54+
sandbox_image_pull_policy: Option<String>,
55+
5256
/// gRPC endpoint for sandboxes to callback to `OpenShell`.
5357
/// This should be reachable from within the Kubernetes cluster.
5458
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
@@ -157,6 +161,10 @@ async fn main() -> Result<()> {
157161
config = config.with_sandbox_image(image);
158162
}
159163

164+
if let Some(policy) = args.sandbox_image_pull_policy {
165+
config = config.with_sandbox_image_pull_policy(policy);
166+
}
167+
160168
if let Some(endpoint) = args.grpc_endpoint {
161169
config = config.with_grpc_endpoint(endpoint);
162170
}

crates/navigator-server/src/sandbox/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub struct SandboxClient {
3737
client: Client,
3838
namespace: String,
3939
default_image: String,
40+
/// Kubernetes `imagePullPolicy` for sandbox containers. When empty the
41+
/// field is omitted from the pod spec and Kubernetes applies its default.
42+
image_pull_policy: String,
4043
grpc_endpoint: String,
4144
ssh_listen_addr: String,
4245
ssh_handshake_secret: String,
@@ -59,6 +62,7 @@ impl SandboxClient {
5962
pub async fn new(
6063
namespace: String,
6164
default_image: String,
65+
image_pull_policy: String,
6266
grpc_endpoint: String,
6367
ssh_listen_addr: String,
6468
ssh_handshake_secret: String,
@@ -79,6 +83,7 @@ impl SandboxClient {
7983
client,
8084
namespace,
8185
default_image,
86+
image_pull_policy,
8287
grpc_endpoint,
8388
ssh_listen_addr,
8489
ssh_handshake_secret,
@@ -149,6 +154,7 @@ impl SandboxClient {
149154
obj.data = sandbox_to_k8s_spec(
150155
sandbox.spec.as_ref(),
151156
&self.default_image,
157+
&self.image_pull_policy,
152158
&sandbox.id,
153159
&sandbox.name,
154160
&self.grpc_endpoint,
@@ -721,6 +727,7 @@ fn apply_supervisor_bootstrap(pod_template: &mut serde_json::Value, default_imag
721727
fn sandbox_to_k8s_spec(
722728
spec: Option<&SandboxSpec>,
723729
default_image: &str,
730+
image_pull_policy: &str,
724731
sandbox_id: &str,
725732
sandbox_name: &str,
726733
grpc_endpoint: &str,
@@ -746,6 +753,7 @@ fn sandbox_to_k8s_spec(
746753
sandbox_template_to_k8s(
747754
template,
748755
default_image,
756+
image_pull_policy,
749757
sandbox_id,
750758
sandbox_name,
751759
grpc_endpoint,
@@ -777,6 +785,7 @@ fn sandbox_to_k8s_spec(
777785
sandbox_template_to_k8s(
778786
&SandboxTemplate::default(),
779787
default_image,
788+
image_pull_policy,
780789
sandbox_id,
781790
sandbox_name,
782791
grpc_endpoint,
@@ -798,6 +807,7 @@ fn sandbox_to_k8s_spec(
798807
fn sandbox_template_to_k8s(
799808
template: &SandboxTemplate,
800809
default_image: &str,
810+
image_pull_policy: &str,
801811
sandbox_id: &str,
802812
sandbox_name: &str,
803813
grpc_endpoint: &str,
@@ -812,6 +822,7 @@ fn sandbox_template_to_k8s(
812822
pod_template,
813823
template,
814824
default_image,
825+
image_pull_policy,
815826
sandbox_id,
816827
sandbox_name,
817828
grpc_endpoint,
@@ -861,6 +872,12 @@ fn sandbox_template_to_k8s(
861872
};
862873
if !image.is_empty() {
863874
container.insert("image".to_string(), serde_json::json!(image));
875+
if !image_pull_policy.is_empty() {
876+
container.insert(
877+
"imagePullPolicy".to_string(),
878+
serde_json::json!(image_pull_policy),
879+
);
880+
}
864881
}
865882

866883
// Build environment variables - start with OpenShell-required vars
@@ -945,6 +962,7 @@ fn inject_pod_template(
945962
mut pod_template: serde_json::Value,
946963
template: &SandboxTemplate,
947964
default_image: &str,
965+
image_pull_policy: &str,
948966
sandbox_id: &str,
949967
sandbox_name: &str,
950968
grpc_endpoint: &str,
@@ -1006,6 +1024,16 @@ fn inject_pod_template(
10061024
!client_tls_secret_name.is_empty(),
10071025
);
10081026

1027+
// Inject imagePullPolicy on the agent container.
1028+
if !image_pull_policy.is_empty() {
1029+
if let Some(container_obj) = container.as_object_mut() {
1030+
container_obj.insert(
1031+
"imagePullPolicy".to_string(),
1032+
serde_json::json!(image_pull_policy),
1033+
);
1034+
}
1035+
}
1036+
10091037
// Inject TLS volumeMount on the agent container.
10101038
if !client_tls_secret_name.is_empty()
10111039
&& let Some(container_obj) = container.as_object_mut()

deploy/helm/openshell/templates/statefulset.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ spec:
5151
value: {{ .Values.server.sandboxNamespace | quote }}
5252
- name: OPENSHELL_SANDBOX_IMAGE
5353
value: {{ .Values.server.sandboxImage | quote }}
54+
{{- if .Values.server.sandboxImagePullPolicy }}
55+
- name: OPENSHELL_SANDBOX_IMAGE_PULL_POLICY
56+
value: {{ .Values.server.sandboxImagePullPolicy | quote }}
57+
{{- end }}
5458
- name: OPENSHELL_GRPC_ENDPOINT
5559
value: {{ if .Values.server.disableTls }}{{ .Values.server.grpcEndpoint | replace "https://" "http://" | quote }}{{ else }}{{ .Values.server.grpcEndpoint | quote }}{{ end }}
5660
{{- if .Values.server.sshGatewayHost }}

deploy/helm/openshell/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ server:
6868
sandboxNamespace: openshell
6969
dbUrl: "sqlite:/var/openshell/openshell.db"
7070
sandboxImage: "ghcr.io/nvidia/openshell/sandbox:latest"
71+
# Kubernetes imagePullPolicy for sandbox pods. Empty = Kubernetes default
72+
# (Always for :latest, IfNotPresent otherwise). Set to "Always" for dev
73+
# clusters so new images are picked up without manual eviction.
74+
sandboxImagePullPolicy: ""
7175
# gRPC endpoint for sandboxes to callback to OpenShell (must be reachable from pods)
7276
grpcEndpoint: "https://openshell.openshell.svc.cluster.local:8080"
7377
# Public host/port returned to CLI clients for SSH proxy CONNECT requests.

tasks/scripts/cluster-deploy-fast.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ if [[ "${needs_helm_upgrade}" == "1" ]]; then
411411
--set image.pullPolicy=Always \
412412
--set-string server.grpcEndpoint=https://openshell.openshell.svc.cluster.local:8080 \
413413
--set server.sandboxImage=${IMAGE_REPO_BASE}/sandbox:${IMAGE_TAG} \
414+
--set server.sandboxImagePullPolicy=Always \
414415
--set server.tls.certSecretName=openshell-server-tls \
415416
--set server.tls.clientCaSecretName=openshell-server-client-ca \
416417
--set server.tls.clientTlsSecretName=openshell-client-tls \

0 commit comments

Comments
 (0)