Skip to content

Commit d917970

Browse files
committed
fix: forward registry credentials from image name to HTTP Basic Auth
- Add Username/Password fields to DockerHubRegistry - Add NewDockerHubRegistryWithCreds constructor - Update FetchManifest and FetchLayer to use http.NewRequest + SetBasicAuth - Add extractCredentials() to parse user:pass@ prefix from image name - Wire credentials through run() → NewDockerHubRegistryWithCreds - Add TestExtractCredentials unit test Fixes verify.sh CI failure: 'user:password@localhost:5000/alpine' was returning 401 because credentials were stripped by resolveRegistry but never sent in the manifest/layer HTTP requests.
1 parent 5e3fa34 commit d917970

3 files changed

Lines changed: 75 additions & 4 deletions

File tree

image.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ type Registry interface {
7272

7373
// DockerHubRegistry is a default implementation of the Registry interface for GHCR or custom registries.
7474
type DockerHubRegistry struct {
75-
BaseURL string
75+
BaseURL string
76+
Username string
77+
Password string
7678
}
7779

7880
// NewDockerHubRegistry creates a new instance of DockerHubRegistry with an optional custom registry URL.
@@ -85,10 +87,25 @@ func NewDockerHubRegistry(customURL string) *DockerHubRegistry {
8587
}
8688
}
8789

90+
// NewDockerHubRegistryWithCreds creates a DockerHubRegistry that sends HTTP Basic Auth on every request.
91+
func NewDockerHubRegistryWithCreds(customURL, username, password string) *DockerHubRegistry {
92+
r := NewDockerHubRegistry(customURL)
93+
r.Username = username
94+
r.Password = password
95+
return r
96+
}
97+
8898
// FetchManifest fetches the manifest for a given repository and tag.
8999
func (r *DockerHubRegistry) FetchManifest(repo, tag string) (*Manifest, error) {
90100
url := fmt.Sprintf("%s%s/manifests/%s", r.BaseURL, repo, tag)
91-
resp, err := http.Get(url)
101+
req, err := http.NewRequest(http.MethodGet, url, nil)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to create manifest request: %w", err)
104+
}
105+
if r.Username != "" {
106+
req.SetBasicAuth(r.Username, r.Password)
107+
}
108+
resp, err := http.DefaultClient.Do(req)
92109
if err != nil {
93110
return nil, fmt.Errorf("failed to fetch manifest: %w", err)
94111
}
@@ -109,7 +126,14 @@ func (r *DockerHubRegistry) FetchManifest(repo, tag string) (*Manifest, error) {
109126
// FetchLayer fetches a specific layer by its digest.
110127
func (r *DockerHubRegistry) FetchLayer(repo, digest string) (io.ReadCloser, error) {
111128
url := fmt.Sprintf("%s%s/blobs/%s", r.BaseURL, repo, digest)
112-
resp, err := http.Get(url)
129+
req, err := http.NewRequest(http.MethodGet, url, nil)
130+
if err != nil {
131+
return nil, fmt.Errorf("failed to create layer request: %w", err)
132+
}
133+
if r.Username != "" {
134+
req.SetBasicAuth(r.Username, r.Password)
135+
}
136+
resp, err := http.DefaultClient.Do(req)
113137
if err != nil {
114138
return nil, fmt.Errorf("failed to fetch layer: %w", err)
115139
}

main.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,8 +552,9 @@ func run() {
552552
} else {
553553
fmt.Printf("Fetching image '%s' from registry...\n", imageName)
554554
registryURL, repo := resolveRegistry(imageName)
555+
username, password := extractCredentials(imageName)
555556

556-
registry := NewDockerHubRegistry(registryURL)
557+
registry := NewDockerHubRegistryWithCreds(registryURL, username, password)
557558
image, err := Pull(registry, repo)
558559
if err != nil {
559560
fmt.Printf("Error: Failed to fetch image '%s': %v\n", imageName, err)
@@ -629,6 +630,26 @@ func resolveRegistry(imageName string) (string, string) {
629630
return registryURL, repo
630631
}
631632

633+
// extractCredentials parses "user:pass@host/repo" and returns (username, password).
634+
// Returns empty strings when no credentials are present.
635+
func extractCredentials(imageName string) (string, string) {
636+
parts := strings.SplitN(imageName, "/", 2)
637+
if len(parts) != 2 {
638+
return "", ""
639+
}
640+
hostPart := parts[0]
641+
at := strings.LastIndex(hostPart, "@")
642+
if at < 0 {
643+
return "", ""
644+
}
645+
creds := hostPart[:at]
646+
colon := strings.Index(creds, ":")
647+
if colon < 0 {
648+
return creds, ""
649+
}
650+
return creds[:colon], creds[colon+1:]
651+
}
652+
632653
func registryURLForHost(host string) string {
633654
if isLocalRegistryHost(host) {
634655
return fmt.Sprintf("http://%s/v2/", host)

main_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,32 @@ func TestResolveRegistry(t *testing.T) {
185185
}
186186
}
187187

188+
func TestExtractCredentials(t *testing.T) {
189+
tests := []struct {
190+
name string
191+
imageName string
192+
wantUser string
193+
wantPass string
194+
}{
195+
{"no credentials", "localhost:5000/alpine", "", ""},
196+
{"user and password", "user:password@localhost:5000/alpine", "user", "password"},
197+
{"email username", "testuser@example.com:testpass@localhost:5000/alpine:latest", "testuser@example.com", "testpass"},
198+
{"no slash", "alpine:latest", "", ""},
199+
{"username only (no colon)", "user@localhost:5000/alpine", "user", ""},
200+
}
201+
for _, tt := range tests {
202+
t.Run(tt.name, func(t *testing.T) {
203+
gotUser, gotPass := extractCredentials(tt.imageName)
204+
if gotUser != tt.wantUser {
205+
t.Fatalf("username: got %q, want %q", gotUser, tt.wantUser)
206+
}
207+
if gotPass != tt.wantPass {
208+
t.Fatalf("password: got %q, want %q", gotPass, tt.wantPass)
209+
}
210+
})
211+
}
212+
}
213+
188214
// TestCapsuleManager:
189215
// - Verifies the CapsuleManager's functionality, including adding, retrieving, and attaching Resource Capsules.
190216
// - Setup: Initializes a CapsuleManager instance.

0 commit comments

Comments
 (0)