From d5c515ab2e6b171433856cde20c351c1af1fc458 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 23 Oct 2026 22:57:01 -0400 Subject: [PATCH] fix(isolation): apply the configured registry to namespaced images --- internal/upstream/core/isolation.go | 26 +++++++++++----- .../core/isolation_registry_namespace_test.go | 30 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 internal/upstream/core/isolation_registry_namespace_test.go diff --git a/internal/upstream/core/isolation.go b/internal/upstream/core/isolation.go index f7f610b54..b230cd38a 100644 --- a/internal/upstream/core/isolation.go +++ b/internal/upstream/core/isolation.go @@ -472,8 +472,13 @@ func (im *IsolationManager) ResolveDefaults(serverConfig *config.ServerConfig) * // buildFullImageName constructs the full image name with registry if needed func (im *IsolationManager) buildFullImageName(image string) string { - // If image already contains a registry (has a slash before the first colon), use as-is - if strings.Contains(image, "/") && strings.Index(image, "/") < strings.Index(image, ":") { + if imageHasRegistryHost(image) { + return image + } + + // Without a configured registry, a namespaced Docker Hub reference + // (org/image) is already pullable as written. + if im.globalConfig.Registry == "" && strings.Contains(image, "/") { return image } @@ -483,11 +488,6 @@ func (im *IsolationManager) buildFullImageName(image string) string { registry = "docker.io" } - // Don't prepend registry if image already contains one - if strings.Contains(image, "/") { - return image - } - // For official images (no slash), prepend library/ if !strings.Contains(image, "/") { return fmt.Sprintf("%s/library/%s", registry, image) @@ -496,6 +496,18 @@ func (im *IsolationManager) buildFullImageName(image string) string { return fmt.Sprintf("%s/%s", registry, image) } +// imageHasRegistryHost applies Docker's reference rule: the first path +// component names a registry only if it contains '.' or ':' or is +// "localhost"; otherwise it is a Docker Hub namespace (org/image). +func imageHasRegistryHost(image string) bool { + i := strings.Index(image, "/") + if i < 0 { + return false + } + host := image[:i] + return strings.ContainsAny(host, ".:") || host == "localhost" +} + // BuildDockerArgs constructs Docker run arguments for isolation func (im *IsolationManager) BuildDockerArgs(serverConfig *config.ServerConfig, runtimeType string) ([]string, error) { image, err := im.GetDockerImage(serverConfig, runtimeType) diff --git a/internal/upstream/core/isolation_registry_namespace_test.go b/internal/upstream/core/isolation_registry_namespace_test.go new file mode 100644 index 000000000..408d11143 --- /dev/null +++ b/internal/upstream/core/isolation_registry_namespace_test.go @@ -0,0 +1,30 @@ +package core + +import "testing" + +func TestBuildFullImageName_ConfiguredRegistryPrefixesNamespacedImages(t *testing.T) { + mirror := registryOnlyManager("mirror.corp", nil) + cases := map[string]string{ + "myorg/node-mcp:1": "mirror.corp/myorg/node-mcp:1", + "python:3.11": "mirror.corp/library/python:3.11", + "ghcr.io/org/img:2": "ghcr.io/org/img:2", + "localhost:5000/team/img": "localhost:5000/team/img", + "localhost/team/img": "localhost/team/img", + "registry.local/img@sha256:abc": "registry.local/img@sha256:abc", + } + for in, want := range cases { + if got := mirror.buildFullImageName(in); got != want { + t.Errorf("registry=mirror.corp: buildFullImageName(%q) = %q, want %q", in, got, want) + } + } + + plain := registryOnlyManager("", nil) + for in, want := range map[string]string{ + "myorg/node-mcp:1": "myorg/node-mcp:1", + "python:3.11": "docker.io/library/python:3.11", + } { + if got := plain.buildFullImageName(in); got != want { + t.Errorf("no registry: buildFullImageName(%q) = %q, want %q", in, got, want) + } + } +}