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
- ee69b9e2f39fcb70b8ade7b95ff214459188b35d
- author
- bnema · · 2026-02-03 20:11 UTC
- parent
- 63aec82
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +51 -17
M internal/usecase/container/service.go +44 -17
| @@ -642,28 +642,55 @@ func (s *Service) buildImageRef(image string) string { | |||
| 642 | 642 | return image | |
| 643 | 643 | } | |
| 644 | 644 | ||
| 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 | |
| 656 | 675 | } | |
| 657 | 676 | } | |
| 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 | |
| 661 | 681 | } | |
| 662 | 682 | ||
| 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 | + | } | |
| 665 | 692 | } | |
| 666 | - | return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image) | |
| 693 | + | return true | |
| 667 | 694 | } | |
| 668 | 695 | ||
| 669 | 696 | func normalizePullPolicy(policy string) string { | |
| @@ -642,28 +642,55 @@ func (s *Service) buildImageRef(image string) string { | |||||
| 642 | return image | 642 | 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 := image | 650 | + | 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 image | 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 | + | } | |||
| 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) { | |||
| 793 | 793 | registryDomain: "localhost:5000", | |
| 794 | 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 | 805 | 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 { | ||