detached ff6f00c

fix(container): detect explicit registry in host:port format

Add test case for images with different host:port than configured
RegistryDomain. Refactor buildImageRef to properly detect explicit
registries including host:port patterns (e.g., localhost:5001/app)
not just domain patterns (e.g., docker.io/app).
commit
ff6f00c5d6b34efccbc3301f2c798338c95a1b5f
author
bnema · · 2026-02-03 20:11 UTC
parent
1b9113b
signature
unsigned
2 files · +51 -17
M internal/usecase/container/service.go +44 -17
@@ -642,28 +642,55 @@ func (s *Service) buildImageRef(image string) string {
642642 return image
643643 }
644644
645- // Check if image is from an external registry (has dot and slash in the repo part).
646- // Extract repo part by finding tag separator (last colon not in digest).
647- // For "docker.io/library/nginx:latest" -> "docker.io/library/nginx"
648- // For "myapp:latest" -> "myapp"
649- // For "localhost:5000/myapp:latest" -> "localhost:5000/myapp" (already handled above)
650- repoPart := image
651- if idx := strings.LastIndex(image, ":"); idx != -1 {
652- // Check if this colon is a tag separator (not part of a port)
653- afterColon := image[idx+1:]
654- if !strings.Contains(afterColon, "/") {
655- repoPart = image[:idx]
645+ // Check if image already has an explicit registry (host:port/ or host.domain/).
646+ // We need to detect patterns like:
647+ // - "docker.io/library/nginx:latest" (has dot and slash)
648+ // - "localhost:5001/myapp:latest" (has host:port/ pattern)
649+ // - "myregistry:5000/app:v1" (has host:port/ pattern)
650+ if hasExplicitRegistry(image) {
651+ return image
652+ }
653+
654+ return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)
655+}
656+
657+// hasExplicitRegistry checks if an image reference already includes an explicit registry.
658+// Returns true for patterns like "docker.io/image", "localhost:5000/image", "registry:8080/image".
659+func hasExplicitRegistry(image string) bool {
660+ // Find the first slash which separates registry from image name
661+ slashIdx := strings.Index(image, "/")
662+ if slashIdx == -1 {
663+ return false // No slash means no registry prefix (e.g., "myapp:latest")
664+ }
665+
666+ // Extract the part before the first slash (potential registry)
667+ registryPart := image[:slashIdx]
668+
669+ // Check for host:port pattern (e.g., "localhost:5000", "registry:8080")
670+ if colonIdx := strings.LastIndex(registryPart, ":"); colonIdx != -1 {
671+ port := registryPart[colonIdx+1:]
672+ // If everything after colon is digits, it's a port
673+ if len(port) > 0 && isNumeric(port) {
674+ return true
656675 }
657676 }
658- // Handle digest references: repo@sha256:...
659- if idx := strings.Index(repoPart, "@"); idx != -1 {
660- repoPart = repoPart[:idx]
677+
678+ // Check for domain pattern (has a dot, e.g., "docker.io", "gcr.io")
679+ if strings.Contains(registryPart, ".") {
680+ return true
661681 }
662682
663- if strings.Contains(repoPart, ".") && strings.Contains(repoPart, "/") {
664- return image
683+ return false
684+}
685+
686+// isNumeric checks if a string contains only digits.
687+func isNumeric(s string) bool {
688+ for _, c := range s {
689+ if c < '0' || c > '9' {
690+ return false
691+ }
665692 }
666- return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)
693+ return true
667694 }
668695
669696 func normalizePullPolicy(policy string) string {
@@ -642,28 +642,55 @@ func (s *Service) buildImageRef(image string) string {
642 return image642 return image
643 }643 }
644 644
645- // Check if image is from an external registry (has dot and slash in the repo part).645+ // Check if image already has an explicit registry (host:port/ or host.domain/).
646- // Extract repo part by finding tag separator (last colon not in digest).646+ // We need to detect patterns like:
647- // For "docker.io/library/nginx:latest" -> "docker.io/library/nginx"647+ // - "docker.io/library/nginx:latest" (has dot and slash)
648- // For "myapp:latest" -> "myapp"648+ // - "localhost:5001/myapp:latest" (has host:port/ pattern)
649- // For "localhost:5000/myapp:latest" -> "localhost:5000/myapp" (already handled above)649+ // - "myregistry:5000/app:v1" (has host:port/ pattern)
650- repoPart := image650+ if hasExplicitRegistry(image) {
651- if idx := strings.LastIndex(image, ":"); idx != -1 {651+ return image
652- // Check if this colon is a tag separator (not part of a port)652+ }
653- afterColon := image[idx+1:]653+
654- if !strings.Contains(afterColon, "/") {654+ return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)
655- repoPart = image[:idx]655+}
656+
657+// hasExplicitRegistry checks if an image reference already includes an explicit registry.
658+// Returns true for patterns like "docker.io/image", "localhost:5000/image", "registry:8080/image".
659+func hasExplicitRegistry(image string) bool {
660+ // Find the first slash which separates registry from image name
661+ slashIdx := strings.Index(image, "/")
662+ if slashIdx == -1 {
663+ return false // No slash means no registry prefix (e.g., "myapp:latest")
664+ }
665+
666+ // Extract the part before the first slash (potential registry)
667+ registryPart := image[:slashIdx]
668+
669+ // Check for host:port pattern (e.g., "localhost:5000", "registry:8080")
670+ if colonIdx := strings.LastIndex(registryPart, ":"); colonIdx != -1 {
671+ port := registryPart[colonIdx+1:]
672+ // If everything after colon is digits, it's a port
673+ if len(port) > 0 && isNumeric(port) {
674+ return true
656 }675 }
657 }676 }
658- // Handle digest references: repo@sha256:...677+
659- if idx := strings.Index(repoPart, "@"); idx != -1 {678+ // Check for domain pattern (has a dot, e.g., "docker.io", "gcr.io")
660- repoPart = repoPart[:idx]679+ if strings.Contains(registryPart, ".") {
680+ return true
661 }681 }
662 682
663- if strings.Contains(repoPart, ".") && strings.Contains(repoPart, "/") {683+ return false
664- return image684+}
685+
686+// isNumeric checks if a string contains only digits.
687+func isNumeric(s string) bool {
688+ for _, c := range s {
689+ if c < '0' || c > '9' {
690+ return false
691+ }
665 }692 }
666- return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)693+ return true
667 }694 }
668 695
669 func normalizePullPolicy(policy string) string {696 func normalizePullPolicy(policy string) string {
M internal/usecase/container/service_test.go +7 -0
@@ -793,6 +793,13 @@ func TestBuildImageRef(t *testing.T) {
793793 registryDomain: "localhost:5000",
794794 wantRef: "localhost:5000/myapp:latest",
795795 },
796+ {
797+ name: "explicit host:port different than RegistryDomain",
798+ image: "localhost:5001/myapp:latest",
799+ registryAuthEnabled: true,
800+ registryDomain: "localhost:5000",
801+ wantRef: "localhost:5001/myapp:latest",
802+ },
796803 }
797804
798805 for _, tt := range tests {
@@ -793,6 +793,13 @@ func TestBuildImageRef(t *testing.T) {
793 registryDomain: "localhost:5000",793 registryDomain: "localhost:5000",
794 wantRef: "localhost:5000/myapp:latest",794 wantRef: "localhost:5000/myapp:latest",
795 },795 },
796+ {
797+ name: "explicit host:port different than RegistryDomain",
798+ image: "localhost:5001/myapp:latest",
799+ registryAuthEnabled: true,
800+ registryDomain: "localhost:5000",
801+ wantRef: "localhost:5001/myapp:latest",
802+ },
796 }803 }
797 804
798 for _, tt := range tests {805 for _, tt := range tests {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close