Skip to content

Commit 0ec2cb0

Browse files
yroblataskbotCopilot
authored
feat: shorten generated container name (#1220)
Co-authored-by: taskbot <taskbot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5e9ef2d commit 0ec2cb0

File tree

2 files changed

+124
-11
lines changed

2 files changed

+124
-11
lines changed

pkg/container/name.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,44 @@ func GetOrGenerateContainerName(containerName, image string) (string, string) {
2727

2828
// generateContainerBaseName generates a base name for a container from the image name
2929
func generateContainerBaseName(image string) string {
30-
// Extract the base name from the image, preserving registry namespaces
31-
// Examples:
32-
// - "nginx:latest" -> "nginx"
33-
// - "docker.io/library/nginx:latest" -> "docker.io-library-nginx"
34-
// - "quay.io/stacklok/mcp-server:v1" -> "quay.io-stacklok-mcp-server"
30+
// Find last '/' and last ':' to distinguish port from tag
31+
lastSlash := strings.LastIndex(image, "/")
32+
lastColon := strings.LastIndex(image, ":")
3533

36-
// First, remove the tag part (everything after the colon)
37-
imageWithoutTag := strings.Split(image, ":")[0]
34+
imageWithoutTag := image
35+
if lastColon > lastSlash {
36+
imageWithoutTag = image[:lastColon]
37+
}
38+
39+
// Split by '/'
40+
parts := strings.Split(imageWithoutTag, "/")
41+
var registryOrNamespace, name string
42+
switch len(parts) {
43+
case 1:
44+
name = parts[0]
45+
case 2:
46+
registryOrNamespace = parts[0]
47+
name = parts[1]
48+
default:
49+
registryOrNamespace = parts[len(parts)-2]
50+
name = parts[len(parts)-1]
51+
}
52+
// Strip the port from registryOrNamespace if it looks like host:port
53+
if registryOrNamespace != "" && strings.Contains(registryOrNamespace, ":") {
54+
registryOrNamespace = strings.SplitN(registryOrNamespace, ":", 2)[0]
55+
}
3856

39-
// Replace slashes with dashes to preserve namespace structure
40-
namespaceName := strings.ReplaceAll(imageWithoutTag, "/", "-")
57+
// Construct the base name using the sanitized registryOrNamespace and name
58+
var base string
59+
if registryOrNamespace != "" {
60+
base = registryOrNamespace + "-" + name
61+
} else {
62+
base = name
63+
}
4164

42-
// Sanitize the name (allow alphanumeric, dashes)
65+
// Sanitize: allow alphanumeric and dashes
4366
var sanitizedName strings.Builder
44-
for _, c := range namespaceName {
67+
for _, c := range base {
4568
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' {
4669
sanitizedName.WriteRune(c)
4770
} else {

pkg/container/name_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package container
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGenerateContainerBaseName(t *testing.T) {
10+
t.Parallel()
11+
tests := []struct {
12+
name string
13+
image string
14+
expected string
15+
}{
16+
{
17+
name: "no namespace, with tag",
18+
image: "nginx:latest",
19+
expected: "nginx",
20+
},
21+
{
22+
name: "namespace and image, with tag",
23+
image: "library/nginx:latest",
24+
expected: "library-nginx",
25+
},
26+
{
27+
name: "registry, namespace, image, with tag",
28+
image: "docker.io/library/nginx:latest",
29+
expected: "library-nginx",
30+
},
31+
{
32+
name: "deep registry, multiple namespaces, image, with tag",
33+
image: "quay.io/stacklok/mcp-server:v1",
34+
expected: "stacklok-mcp-server",
35+
},
36+
{
37+
name: "simple image, no tag",
38+
image: "server",
39+
expected: "server",
40+
},
41+
{
42+
name: "namespace, image, no tag",
43+
image: "stacklok/server",
44+
expected: "stacklok-server",
45+
},
46+
{
47+
name: "multiple slashes, should pick last two",
48+
image: "a/b/c/d:foo",
49+
expected: "c-d",
50+
},
51+
{
52+
name: "image with special characters",
53+
image: "foo/bar@sha256:abcdef",
54+
expected: "foo-bar-sha256",
55+
},
56+
{
57+
name: "localhost registry with port",
58+
image: "localhost:5000/image:latest",
59+
expected: "localhost-image",
60+
},
61+
{
62+
name: "very deep path",
63+
image: "x/y/z/w/foo:bar",
64+
expected: "w-foo",
65+
},
66+
{
67+
name: "empty image name",
68+
image: "",
69+
expected: "",
70+
},
71+
{
72+
name: "single slash (should treat as namespace-image)",
73+
image: "foo/bar",
74+
expected: "foo-bar",
75+
},
76+
{
77+
name: "single image with special chars",
78+
image: "my$image:latest",
79+
expected: "my-image",
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
t.Parallel()
86+
got := generateContainerBaseName(tt.image)
87+
assert.Equal(t, tt.expected, got, "generateContainerBaseName(%q)", tt.image)
88+
})
89+
}
90+
}

0 commit comments

Comments
 (0)