Skip to content

Commit 9333185

Browse files
committed
add more cases to handle the auth config | refactor error handling
Signed-off-by: mabulgu <mabulgu@gmail.com>
1 parent 64263dc commit 9333185

2 files changed

Lines changed: 200 additions & 26 deletions

File tree

pkg/secretutils/dockerconfig.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package secretutils
33
import (
44
"encoding/base64"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/url"
89
"strings"
@@ -32,7 +33,7 @@ type DockerConfig map[string]DockerAuthConfig
3233
// Returns the credentials in base64-encoded "username:password" format as expected by Ironic.
3334
func ExtractRegistryCredentials(secret *corev1.Secret, imageURL string) (string, error) {
3435
if secret == nil {
35-
return "", fmt.Errorf("secret is nil")
36+
return "", errors.New("secret is nil")
3637
}
3738

3839
registryHost, err := extractRegistryHost(imageURL)
@@ -74,7 +75,7 @@ func ExtractRegistryCredentials(secret *corev1.Secret, imageURL string) (string,
7475
}
7576

7677
// extractRegistryHost extracts the registry hostname from an OCI image URL.
77-
// For example, "oci://registry.example.com/repo/image:tag" returns "registry.example.com"
78+
// For example, "oci://registry.example.com/repo/image:tag" returns "registry.example.com".
7879
func extractRegistryHost(imageURL string) (string, error) {
7980
if !strings.HasPrefix(imageURL, "oci://") {
8081
return "", fmt.Errorf("image URL does not have oci:// scheme: %s", imageURL)
@@ -127,8 +128,9 @@ func parseDockerConfig(data []byte, registryHost string) (*DockerAuthConfig, err
127128

128129
// findAuthConfig searches for the auth config matching the registry host.
129130
// It tries several variations of the registry host to handle different formats.
131+
// Returns a clear RegistryEntryMissing error when no entry matches.
130132
func findAuthConfig(auths map[string]DockerAuthConfig, registryHost string) (*DockerAuthConfig, error) {
131-
// Try exact match first
133+
// Try exact match first (handles both "host" and "host:port")
132134
if authConfig, ok := auths[registryHost]; ok {
133135
return &authConfig, nil
134136
}
@@ -153,20 +155,36 @@ func findAuthConfig(auths map[string]DockerAuthConfig, registryHost string) (*Do
153155
return &authConfig, nil
154156
}
155157

156-
// For Docker Hub, try common variations
157-
if strings.Contains(registryHost, "docker.io") || registryHost == "docker.io" {
158-
for _, key := range []string{
158+
// Try with https:// prefix and /v1/ suffix
159+
if authConfig, ok := auths["https://"+registryHost+"/v1/"]; ok {
160+
return &authConfig, nil
161+
}
162+
163+
// Try with https:// prefix and /v2/ suffix
164+
if authConfig, ok := auths["https://"+registryHost+"/v2/"]; ok {
165+
return &authConfig, nil
166+
}
167+
168+
// Special handling for Docker Hub
169+
// Docker Hub can appear as: docker.io, index.docker.io, https://index.docker.io/v1/, etc.
170+
if registryHost == "docker.io" || registryHost == "index.docker.io" ||
171+
strings.HasPrefix(registryHost, "docker.io:") || strings.HasPrefix(registryHost, "index.docker.io:") {
172+
dockerHubKeys := []string{
159173
"https://index.docker.io/v1/",
160174
"index.docker.io",
161175
"docker.io",
162176
"https://docker.io",
163-
} {
177+
"https://index.docker.io",
178+
registryHost, // Already tried but keep for clarity
179+
}
180+
for _, key := range dockerHubKeys {
164181
if authConfig, ok := auths[key]; ok {
165182
return &authConfig, nil
166183
}
167184
}
168185
}
169186

187+
// Return clear error when no entry matches
170188
return nil, fmt.Errorf("registry %s not found in auth config", registryHost)
171189
}
172190

@@ -185,14 +203,14 @@ func extractCredentials(authConfig *DockerAuthConfig) (username, password string
185203
return "", "", fmt.Errorf("failed to decode auth field: %w", err)
186204
}
187205

188-
parts := strings.SplitN(string(decoded), ":", 2)
189-
if len(parts) != 2 {
190-
return "", "", fmt.Errorf("invalid auth format: expected username:password")
206+
const credentialParts = 2
207+
parts := strings.SplitN(string(decoded), ":", credentialParts)
208+
if len(parts) != credentialParts {
209+
return "", "", errors.New("invalid auth format: expected username:password")
191210
}
192211

193212
return parts[0], parts[1], nil
194213
}
195214

196-
return "", "", fmt.Errorf("no credentials found in auth config")
215+
return "", "", errors.New("no credentials found in auth config")
197216
}
198-

pkg/secretutils/dockerconfig_test.go

Lines changed: 170 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ func TestExtractRegistryHost(t *testing.T) {
7575

7676
func TestExtractCredentials(t *testing.T) {
7777
tests := []struct {
78-
name string
79-
authConfig *DockerAuthConfig
80-
expectedUser string
81-
expectedPass string
82-
expectError bool
78+
name string
79+
authConfig *DockerAuthConfig
80+
expectedUser string
81+
expectedPass string
82+
expectError bool
8383
}{
8484
{
8585
name: "explicit username and password",
@@ -327,7 +327,10 @@ func TestExtractRegistryCredentials(t *testing.T) {
327327
},
328328
},
329329
}
330-
data, _ := json.Marshal(config)
330+
data, err := json.Marshal(config)
331+
if err != nil {
332+
t.Fatalf("failed to marshal config: %v", err)
333+
}
331334
return data
332335
}
333336

@@ -339,16 +342,19 @@ func TestExtractRegistryCredentials(t *testing.T) {
339342
Password: password,
340343
},
341344
}
342-
data, _ := json.Marshal(config)
345+
data, err := json.Marshal(config)
346+
if err != nil {
347+
t.Fatalf("failed to marshal config: %v", err)
348+
}
343349
return data
344350
}
345351

346352
tests := []struct {
347-
name string
348-
secret *corev1.Secret
349-
imageURL string
350-
expectError bool
351-
validateResult bool
353+
name string
354+
secret *corev1.Secret
355+
imageURL string
356+
expectError bool
357+
validateResult bool
352358
}{
353359
{
354360
name: "dockerconfigjson secret with exact match",
@@ -508,6 +514,10 @@ func TestFindAuthConfig(t *testing.T) {
508514
Username: "user4",
509515
Password: "pass4",
510516
},
517+
"registry.local:5000": {
518+
Username: "user5",
519+
Password: "pass5",
520+
},
511521
}
512522

513523
tests := []struct {
@@ -535,16 +545,33 @@ func TestFindAuthConfig(t *testing.T) {
535545
expectedUser: "user3",
536546
},
537547
{
538-
name: "Docker Hub special case",
548+
name: "Docker Hub special case - docker.io",
539549
registryHost: "docker.io",
540550
expectFound: true,
541551
expectedUser: "user4",
542552
},
543553
{
544-
name: "not found",
554+
name: "Docker Hub special case - index.docker.io",
555+
registryHost: "index.docker.io",
556+
expectFound: true,
557+
expectedUser: "user4",
558+
},
559+
{
560+
name: "registry with port - exact match",
561+
registryHost: "registry.local:5000",
562+
expectFound: true,
563+
expectedUser: "user5",
564+
},
565+
{
566+
name: "missing registry entry",
545567
registryHost: "notfound.registry.com",
546568
expectFound: false,
547569
},
570+
{
571+
name: "missing registry with port",
572+
registryHost: "notfound.registry.com:8080",
573+
expectFound: false,
574+
},
548575
}
549576

550577
for _, tt := range tests {
@@ -554,6 +581,9 @@ func TestFindAuthConfig(t *testing.T) {
554581
if err == nil {
555582
t.Errorf("expected error but got none")
556583
}
584+
if !strings.Contains(err.Error(), "not found in auth config") {
585+
t.Errorf("expected 'not found in auth config' error, got: %v", err)
586+
}
557587
return
558588
}
559589
if err != nil {
@@ -571,3 +601,129 @@ func TestFindAuthConfig(t *testing.T) {
571601
}
572602
}
573603

604+
// TestFindAuthConfigCornerCases tests specific corner cases for registry matching.
605+
func TestFindAuthConfigCornerCases(t *testing.T) {
606+
tests := []struct {
607+
name string
608+
auths map[string]DockerAuthConfig
609+
registryHost string
610+
expectFound bool
611+
expectedUser string
612+
}{
613+
{
614+
name: "registry with port in both",
615+
auths: map[string]DockerAuthConfig{
616+
"registry.example.com:5000": {Username: "portuser", Password: "portpass"},
617+
},
618+
registryHost: "registry.example.com:5000",
619+
expectFound: true,
620+
expectedUser: "portuser",
621+
},
622+
{
623+
name: "registry with http prefix in config",
624+
auths: map[string]DockerAuthConfig{
625+
"http://registry.local": {Username: "httpuser", Password: "httppass"},
626+
},
627+
registryHost: "registry.local",
628+
expectFound: true,
629+
expectedUser: "httpuser",
630+
},
631+
{
632+
name: "registry with https and /v1/ suffix",
633+
auths: map[string]DockerAuthConfig{
634+
"https://registry.example.com/v1/": {Username: "v1user", Password: "v1pass"},
635+
},
636+
registryHost: "registry.example.com",
637+
expectFound: true,
638+
expectedUser: "v1user",
639+
},
640+
{
641+
name: "quay.io exact match",
642+
auths: map[string]DockerAuthConfig{
643+
"quay.io": {Username: "quayuser", Password: "quaypass"},
644+
},
645+
registryHost: "quay.io",
646+
expectFound: true,
647+
expectedUser: "quayuser",
648+
},
649+
{
650+
name: "quay.io with https",
651+
auths: map[string]DockerAuthConfig{
652+
"https://quay.io": {Username: "quayuser", Password: "quaypass"},
653+
},
654+
registryHost: "quay.io",
655+
expectFound: true,
656+
expectedUser: "quayuser",
657+
},
658+
{
659+
name: "gcr.io exact match",
660+
auths: map[string]DockerAuthConfig{
661+
"gcr.io": {Username: "gcruser", Password: "gcrpass"},
662+
},
663+
registryHost: "gcr.io",
664+
expectFound: true,
665+
expectedUser: "gcruser",
666+
},
667+
{
668+
name: "Docker Hub - index.docker.io legacy",
669+
auths: map[string]DockerAuthConfig{
670+
"https://index.docker.io/v1/": {Username: "dockeruser", Password: "dockerpass"},
671+
},
672+
registryHost: "docker.io",
673+
expectFound: true,
674+
expectedUser: "dockeruser",
675+
},
676+
{
677+
name: "Docker Hub - docker.io in config",
678+
auths: map[string]DockerAuthConfig{
679+
"docker.io": {Username: "dockeruser", Password: "dockerpass"},
680+
},
681+
registryHost: "index.docker.io",
682+
expectFound: true,
683+
expectedUser: "dockeruser",
684+
},
685+
{
686+
name: "custom registry with port",
687+
auths: map[string]DockerAuthConfig{
688+
"https://registry.local:8443": {Username: "customuser", Password: "custompass"},
689+
},
690+
registryHost: "registry.local:8443",
691+
expectFound: true,
692+
expectedUser: "customuser",
693+
},
694+
{
695+
name: "registry not in config - clear error",
696+
auths: map[string]DockerAuthConfig{
697+
"other.registry.com": {Username: "otheruser", Password: "otherpass"},
698+
},
699+
registryHost: "missing.registry.com",
700+
expectFound: false,
701+
},
702+
}
703+
704+
for _, tt := range tests {
705+
t.Run(tt.name, func(t *testing.T) {
706+
authConfig, err := findAuthConfig(tt.auths, tt.registryHost)
707+
if !tt.expectFound {
708+
if err == nil {
709+
t.Errorf("expected RegistryEntryMissing error but got none")
710+
}
711+
if !strings.Contains(err.Error(), "not found in auth config") {
712+
t.Errorf("expected clear 'not found in auth config' error, got: %v", err)
713+
}
714+
return
715+
}
716+
if err != nil {
717+
t.Errorf("unexpected error: %v", err)
718+
return
719+
}
720+
if authConfig == nil {
721+
t.Errorf("expected auth config but got nil")
722+
return
723+
}
724+
if authConfig.Username != tt.expectedUser {
725+
t.Errorf("expected username %q, got %q", tt.expectedUser, authConfig.Username)
726+
}
727+
})
728+
}
729+
}

0 commit comments

Comments
 (0)