detached 2b4e30b

fix(container): handle trailing slash and IPv6 registries

- Normalize RegistryDomain by trimming trailing slash to avoid
  double slashes in image references
- Detect bracketed IPv6 addresses (e.g., [fd00::1]/image or
  [fd00::1]:5000/image) as explicit registries
commit
2b4e30bfb535a68b4e650042df2eefb245372ef3
author
bnema · · 2026-02-03 20:41 UTC
parent
855fc04
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +38 -3
M internal/usecase/container/service.go +17 -3
@@ -636,8 +636,11 @@ func (s *Service) buildImageRef(image string) string {
636636 return image
637637 }
638638
639+ // Normalize registry domain by trimming any trailing slash
640+ reg := strings.TrimSuffix(s.config.RegistryDomain, "/")
641+
639642 // Check if image already has the registry domain prefix
640- prefix := s.config.RegistryDomain + "/"
643+ prefix := reg + "/"
641644 if strings.HasPrefix(image, prefix) {
642645 return image
643646 }
@@ -647,15 +650,17 @@ func (s *Service) buildImageRef(image string) string {
647650 // - "docker.io/library/nginx:latest" (has dot and slash)
648651 // - "localhost:5001/myapp:latest" (has host:port/ pattern)
649652 // - "myregistry:5000/app:v1" (has host:port/ pattern)
653+ // - "[fd00::1]:5000/app:v1" (IPv6 with port)
650654 if hasExplicitRegistry(image) {
651655 return image
652656 }
653657
654- return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)
658+ return fmt.Sprintf("%s/%s", reg, image)
655659 }
656660
657661 // hasExplicitRegistry checks if an image reference already includes an explicit registry.
658-// Returns true for patterns like "docker.io/image", "localhost:5000/image", "localhost/image", "registry:8080/image".
662+// Returns true for patterns like "docker.io/image", "localhost:5000/image", "localhost/image",
663+// "registry:8080/image", "[fd00::1]/image", "[fd00::1]:5000/image".
659664 func hasExplicitRegistry(image string) bool {
660665 // Find the first slash which separates registry from image name
661666 slashIdx := strings.Index(image, "/")
@@ -666,6 +671,15 @@ func hasExplicitRegistry(image string) bool {
666671 // Extract the part before the first slash (potential registry)
667672 registryPart := image[:slashIdx]
668673
674+ // Check for bracketed IPv6 address (e.g., "[fd00::1]" or "[fd00::1]:5000")
675+ if strings.HasPrefix(registryPart, "[") {
676+ // Look for closing bracket
677+ if closeBracket := strings.Index(registryPart, "]"); closeBracket != -1 {
678+ // Valid bracketed IPv6: either ends at ] or has ]:port
679+ return true
680+ }
681+ }
682+
669683 // Check for localhost (with or without port)
670684 if registryPart == "localhost" {
671685 return true
@@ -636,8 +636,11 @@ func (s *Service) buildImageRef(image string) string {
636 return image636 return image
637 }637 }
638 638
639+ // Normalize registry domain by trimming any trailing slash
640+ reg := strings.TrimSuffix(s.config.RegistryDomain, "/")
641+
639 // Check if image already has the registry domain prefix642 // Check if image already has the registry domain prefix
640- prefix := s.config.RegistryDomain + "/"643+ prefix := reg + "/"
641 if strings.HasPrefix(image, prefix) {644 if strings.HasPrefix(image, prefix) {
642 return image645 return image
643 }646 }
@@ -647,15 +650,17 @@ func (s *Service) buildImageRef(image string) string {
647 // - "docker.io/library/nginx:latest" (has dot and slash)650 // - "docker.io/library/nginx:latest" (has dot and slash)
648 // - "localhost:5001/myapp:latest" (has host:port/ pattern)651 // - "localhost:5001/myapp:latest" (has host:port/ pattern)
649 // - "myregistry:5000/app:v1" (has host:port/ pattern)652 // - "myregistry:5000/app:v1" (has host:port/ pattern)
653+ // - "[fd00::1]:5000/app:v1" (IPv6 with port)
650 if hasExplicitRegistry(image) {654 if hasExplicitRegistry(image) {
651 return image655 return image
652 }656 }
653 657
654- return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image)658+ return fmt.Sprintf("%s/%s", reg, image)
655 }659 }
656 660
657 // hasExplicitRegistry checks if an image reference already includes an explicit registry.661 // hasExplicitRegistry checks if an image reference already includes an explicit registry.
658-// Returns true for patterns like "docker.io/image", "localhost:5000/image", "localhost/image", "registry:8080/image".662+// Returns true for patterns like "docker.io/image", "localhost:5000/image", "localhost/image",
663+// "registry:8080/image", "[fd00::1]/image", "[fd00::1]:5000/image".
659 func hasExplicitRegistry(image string) bool {664 func hasExplicitRegistry(image string) bool {
660 // Find the first slash which separates registry from image name665 // Find the first slash which separates registry from image name
661 slashIdx := strings.Index(image, "/")666 slashIdx := strings.Index(image, "/")
@@ -666,6 +671,15 @@ func hasExplicitRegistry(image string) bool {
666 // Extract the part before the first slash (potential registry)671 // Extract the part before the first slash (potential registry)
667 registryPart := image[:slashIdx]672 registryPart := image[:slashIdx]
668 673
674+ // Check for bracketed IPv6 address (e.g., "[fd00::1]" or "[fd00::1]:5000")
675+ if strings.HasPrefix(registryPart, "[") {
676+ // Look for closing bracket
677+ if closeBracket := strings.Index(registryPart, "]"); closeBracket != -1 {
678+ // Valid bracketed IPv6: either ends at ] or has ]:port
679+ return true
680+ }
681+ }
682+
669 // Check for localhost (with or without port)683 // Check for localhost (with or without port)
670 if registryPart == "localhost" {684 if registryPart == "localhost" {
671 return true685 return true
M internal/usecase/container/service_test.go +21 -0
@@ -828,6 +828,27 @@ func TestBuildImageRef(t *testing.T) {
828828 registryDomain: "localhost:5000",
829829 wantRef: "localhost/myapp:latest",
830830 },
831+ {
832+ name: "registry domain with trailing slash",
833+ image: "myapp:latest",
834+ registryAuthEnabled: true,
835+ registryDomain: "registry.example.com/",
836+ wantRef: "registry.example.com/myapp:latest",
837+ },
838+ {
839+ name: "ipv6 registry without port",
840+ image: "[fd00::1]/myapp:latest",
841+ registryAuthEnabled: true,
842+ registryDomain: "localhost:5000",
843+ wantRef: "[fd00::1]/myapp:latest",
844+ },
845+ {
846+ name: "ipv6 registry with port",
847+ image: "[fd00::1]:5000/myapp:latest",
848+ registryAuthEnabled: true,
849+ registryDomain: "localhost:5000",
850+ wantRef: "[fd00::1]:5000/myapp:latest",
851+ },
831852 }
832853
833854 for _, tt := range tests {
@@ -828,6 +828,27 @@ func TestBuildImageRef(t *testing.T) {
828 registryDomain: "localhost:5000",828 registryDomain: "localhost:5000",
829 wantRef: "localhost/myapp:latest",829 wantRef: "localhost/myapp:latest",
830 },830 },
831+ {
832+ name: "registry domain with trailing slash",
833+ image: "myapp:latest",
834+ registryAuthEnabled: true,
835+ registryDomain: "registry.example.com/",
836+ wantRef: "registry.example.com/myapp:latest",
837+ },
838+ {
839+ name: "ipv6 registry without port",
840+ image: "[fd00::1]/myapp:latest",
841+ registryAuthEnabled: true,
842+ registryDomain: "localhost:5000",
843+ wantRef: "[fd00::1]/myapp:latest",
844+ },
845+ {
846+ name: "ipv6 registry with port",
847+ image: "[fd00::1]:5000/myapp:latest",
848+ registryAuthEnabled: true,
849+ registryDomain: "localhost:5000",
850+ wantRef: "[fd00::1]:5000/myapp:latest",
851+ },
831 }852 }
832 853
833 for _, tt := range tests {854 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