Skip to content

Commit 79e6f73

Browse files
authored
fix(tui): resolve community image names in sandbox creation (#798)
Extract shared resolve_community_image() into openshell-core so both CLI and TUI expand bare sandbox names (e.g. "base") to full registry references. Previously the TUI passed the bare name directly, causing ImagePullBackOff in Kubernetes. Closes #786
1 parent 7c314e7 commit 79e6f73

File tree

4 files changed

+111
-18
lines changed

4 files changed

+111
-18
lines changed

crates/openshell-cli/src/run.rs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,13 +2455,6 @@ pub async fn sandbox_create(
24552455
}
24562456
}
24572457

2458-
/// The default community sandbox registry prefix.
2459-
///
2460-
/// Bare sandbox names (e.g., `openclaw`) are expanded to
2461-
/// `{prefix}/{name}:latest` using this value. Override with the
2462-
/// `OPENSHELL_COMMUNITY_REGISTRY` environment variable.
2463-
const DEFAULT_COMMUNITY_REGISTRY: &str = "ghcr.io/nvidia/openshell-community/sandboxes";
2464-
24652458
/// Resolved source for the `--from` flag on `sandbox create`.
24662459
enum ResolvedSource {
24672460
/// A ready-to-use container image reference.
@@ -2527,16 +2520,11 @@ fn resolve_from(value: &str) -> Result<ResolvedSource> {
25272520
));
25282521
}
25292522

2530-
// 3. Looks like a full image reference (contains / : or .).
2531-
if value.contains('/') || value.contains(':') || value.contains('.') {
2532-
return Ok(ResolvedSource::Image(value.to_string()));
2533-
}
2534-
2535-
// 4. Community sandbox name.
2536-
let prefix = std::env::var("OPENSHELL_COMMUNITY_REGISTRY")
2537-
.unwrap_or_else(|_| DEFAULT_COMMUNITY_REGISTRY.to_string());
2538-
let prefix = prefix.trim_end_matches('/');
2539-
Ok(ResolvedSource::Image(format!("{prefix}/{value}:latest")))
2523+
// 3. Full image reference or community sandbox name — delegate to shared
2524+
// resolution in openshell-core.
2525+
Ok(ResolvedSource::Image(
2526+
openshell_core::image::resolve_community_image(value),
2527+
))
25402528
}
25412529

25422530
fn source_requests_gpu(source: &str) -> bool {

crates/openshell-core/src/image.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Shared image-name resolution for community sandbox images.
5+
//!
6+
//! Both the CLI and TUI need to expand bare sandbox names (e.g. `"base"`) into
7+
//! fully-qualified container image references. This module centralises that
8+
//! logic so every client resolves names identically.
9+
10+
/// Default registry prefix for community sandbox images.
11+
///
12+
/// Bare sandbox names are expanded to `{prefix}/{name}:latest`.
13+
/// Override at runtime with the `OPENSHELL_COMMUNITY_REGISTRY` env var.
14+
pub const DEFAULT_COMMUNITY_REGISTRY: &str = "ghcr.io/nvidia/openshell-community/sandboxes";
15+
16+
/// Resolve a user-supplied image string into a fully-qualified reference.
17+
///
18+
/// Resolution rules (applied in order):
19+
/// 1. If the value contains `/`, `:`, or `.` it is treated as a complete image
20+
/// reference and returned as-is.
21+
/// 2. Otherwise it is treated as a community sandbox name and expanded to
22+
/// `{registry}/{value}:latest` where `{registry}` defaults to
23+
/// [`DEFAULT_COMMUNITY_REGISTRY`] but can be overridden via the
24+
/// `OPENSHELL_COMMUNITY_REGISTRY` environment variable.
25+
///
26+
/// This function only handles image-name resolution. Dockerfile detection is
27+
/// the responsibility of the caller (e.g. the CLI's `resolve_from()`).
28+
pub fn resolve_community_image(value: &str) -> String {
29+
// Already a fully-qualified reference.
30+
if value.contains('/') || value.contains(':') || value.contains('.') {
31+
return value.to_string();
32+
}
33+
34+
// Community sandbox shorthand → expand with registry prefix.
35+
let prefix = std::env::var("OPENSHELL_COMMUNITY_REGISTRY")
36+
.unwrap_or_else(|_| DEFAULT_COMMUNITY_REGISTRY.to_string());
37+
let prefix = prefix.trim_end_matches('/');
38+
format!("{prefix}/{value}:latest")
39+
}
40+
41+
#[cfg(test)]
42+
#[allow(unsafe_code)]
43+
mod tests {
44+
use super::*;
45+
46+
#[test]
47+
fn bare_name_expands_to_community_registry() {
48+
let result = resolve_community_image("base");
49+
assert_eq!(
50+
result,
51+
"ghcr.io/nvidia/openshell-community/sandboxes/base:latest"
52+
);
53+
}
54+
55+
#[test]
56+
fn bare_name_with_env_override() {
57+
// Use a temp env override. Safety: test-only, and these env-var tests
58+
// are not run concurrently with other tests reading the same var.
59+
let key = "OPENSHELL_COMMUNITY_REGISTRY";
60+
let prev = std::env::var(key).ok();
61+
// SAFETY: single-threaded test context; no other thread reads this var.
62+
unsafe { std::env::set_var(key, "my-registry.example.com/sandboxes") };
63+
let result = resolve_community_image("python");
64+
assert_eq!(result, "my-registry.example.com/sandboxes/python:latest");
65+
// Restore.
66+
match prev {
67+
Some(v) => unsafe { std::env::set_var(key, v) },
68+
None => unsafe { std::env::remove_var(key) },
69+
}
70+
}
71+
72+
#[test]
73+
fn full_reference_with_slash_passes_through() {
74+
let input = "ghcr.io/myorg/myimage:v1";
75+
assert_eq!(resolve_community_image(input), input);
76+
}
77+
78+
#[test]
79+
fn reference_with_colon_passes_through() {
80+
let input = "myimage:latest";
81+
assert_eq!(resolve_community_image(input), input);
82+
}
83+
84+
#[test]
85+
fn reference_with_dot_passes_through() {
86+
let input = "registry.example.com";
87+
assert_eq!(resolve_community_image(input), input);
88+
}
89+
90+
#[test]
91+
fn trailing_slash_in_env_is_trimmed() {
92+
let key = "OPENSHELL_COMMUNITY_REGISTRY";
93+
let prev = std::env::var(key).ok();
94+
// SAFETY: single-threaded test context; no other thread reads this var.
95+
unsafe { std::env::set_var(key, "my-registry.example.com/sandboxes/") };
96+
let result = resolve_community_image("base");
97+
assert_eq!(result, "my-registry.example.com/sandboxes/base:latest");
98+
match prev {
99+
Some(v) => unsafe { std::env::set_var(key, v) },
100+
None => unsafe { std::env::remove_var(key) },
101+
}
102+
}
103+
}

crates/openshell-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
pub mod config;
1313
pub mod error;
1414
pub mod forward;
15+
pub mod image;
1516
pub mod inference;
1617
pub mod paths;
1718
pub mod proto;

crates/openshell-tui/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1274,8 +1274,9 @@ fn spawn_create_sandbox(app: &mut App, tx: mpsc::UnboundedSender<Event>) {
12741274
tokio::spawn(async move {
12751275
let has_custom_image = !image.is_empty();
12761276
let template = if has_custom_image {
1277+
let resolved = openshell_core::image::resolve_community_image(&image);
12771278
Some(openshell_core::proto::SandboxTemplate {
1278-
image,
1279+
image: resolved,
12791280
..Default::default()
12801281
})
12811282
} else {

0 commit comments

Comments
 (0)