-
Notifications
You must be signed in to change notification settings - Fork 82
Add support for public ecr #391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Fernando Perez Osan <[email protected]>
davidcollom
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appreciate the draft PR 👍 - If you're looking for an existing public ECR - we have a deployed version-checker ECR for E2E testing @ public.ecr.aws/c3x5s3k7/public-8456b5e0/alpine which clones alpine:latest
|
Quick update about this, using the |
|
This should be ready for review. Like mentioned before, I stopped using the public ecr library and just performed http requests for it. I tested it with a couple of images, and seems to be working fine. Eg: for kube-proxy
Let me know what you think! |
|
@fer1592 Sorry it's taken so long to get back to this 🙈 - I was curious as to if there was any reasoning around using the OCR Library? or https://github.com/google/go-containerregistry - as a project, we're trying to move away from managing too many client implementations and focusing on the authN/Z portion to the go-containerregistry or native libraries. This is mainly due to maintenance and testing. We don't have a full E2E testing framework for all of the clients, which can make maintaining them difficult and we rely on the community to report issues and raise PR's to resolve issues. |
|
Sorry, been some busy weeks and I forgot to reply! Basically the reasoning was to replicate the same thing than the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for AWS Elastic Container Registry (ECR) Public, enabling version-checker to query image tags from the public.ecr.aws registry. This extends the existing ECR private registry support to handle the publicly accessible AWS container registry.
- Introduces a new
ecrpublicclient package following the existing client implementation pattern - Adds anonymous token-based authentication for ECR Public API access
- Integrates the new client into the main client registry alongside other providers
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/client/ecrpublic/types.go | Defines data structures for ECR Public API responses (AuthResponse and TagResponse) |
| pkg/client/ecrpublic/path.go | Implements host matching (public.ecr.aws) and path parsing for repository/image extraction |
| pkg/client/ecrpublic/path_test.go | Tests for host detection and path parsing logic |
| pkg/client/ecrpublic/ecrpublic.go | Main client implementation with tag fetching, request handling, and anonymous authentication |
| pkg/client/client.go | Registers the ECRPublic client in the main client factory and configuration |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ACR acr.Options | ||
| Docker docker.Options | ||
| ECR ecr.Options | ||
| ECRPublic ecrpublic.Options // Add ECRPublic options |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Remove the inline comment "Add ECRPublic options". Comments that simply describe what the code obviously does add no value and should be removed. The field name ECRPublic is self-documenting.
| ECRPublic ecrpublic.Options // Add ECRPublic options | |
| ECRPublic ecrpublic.Options |
| if opts.Transport != nil { | ||
| opts.Quay.Transporter = opts.Transport | ||
| opts.ECR.Transporter = opts.Transport | ||
| opts.ECRPublic.Transporter = opts.Transport // Add ECR public transporter |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Remove the inline comment "Add ECR public transporter". The code is self-explanatory and the comment adds no value.
| opts.ECRPublic.Transporter = opts.Transport // Add ECR public transporter | |
| opts.ECRPublic.Transporter = opts.Transport |
| } | ||
|
|
||
| func (c *Client) Tags(ctx context.Context, _, repo, image string) ([]api.ImageTag, error) { | ||
| url := fmt.Sprintf(ecrPublicLookupURL, repo, image) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When image is empty (as in the test case "nginx" where repo="nginx" and image=""), the URL will be constructed as https://public.ecr.aws/v2/nginx//tags/list with a double slash. This may not be the correct format for the ECR Public API. Verify that this URL format works correctly, or handle the case where image is empty by constructing the URL differently, e.g., https://public.ecr.aws/v2/nginx/tags/list.
| url := fmt.Sprintf(ecrPublicLookupURL, repo, image) | |
| var url string | |
| if image == "" { | |
| url = fmt.Sprintf("https://public.ecr.aws/v2/%s/tags/list", repo) | |
| } else { | |
| url = fmt.Sprintf(ecrPublicLookupURL, repo, image) | |
| } |
| *http.Client | ||
| Options | ||
| } | ||
|
|
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing compile-time interface assertion. All other client implementations in the codebase include var _ api.ImageClient = (*Client)(nil) to ensure the Client type implements the api.ImageClient interface at compile time. This should be added at the package level (typically after the Client struct definition).
| var _ api.ImageClient = (*Client)(nil) |
| func (c *Client) Tags(ctx context.Context, _, repo, image string) ([]api.ImageTag, error) { | ||
| url := fmt.Sprintf(ecrPublicLookupURL, repo, image) | ||
|
|
||
| var tags []api.ImageTag | ||
| for url != "" { | ||
| response, err := c.doRequest(ctx, url) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| for _, tag := range response.Tags { | ||
| // No images in this result, so continue early | ||
| if len(tag) == 0 { | ||
| continue | ||
| } | ||
|
|
||
| tags = append(tags, api.ImageTag{ | ||
| Tag: tag, | ||
| }) | ||
| } | ||
|
|
||
| url = response.Next | ||
| } | ||
|
|
||
| return tags, nil | ||
| } | ||
|
|
||
| func (c *Client) doRequest(ctx context.Context, url string) (*TagResponse, error) { | ||
| req, err := http.NewRequest(http.MethodGet, url, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Always get a token for ECR Public | ||
| token, err := getAnonymousToken(ctx, c.Client) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get anonymous token: %s", err) | ||
| } | ||
|
|
||
| req.URL.Scheme = "https" | ||
| req = req.WithContext(ctx) | ||
| if len(token) > 0 { | ||
| req.Header.Add("Authorization", "Bearer "+token) | ||
| } | ||
|
|
||
| resp, err := c.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get %q image: %s", c.Name(), err) | ||
| } | ||
| defer func() { _ = resp.Body.Close() }() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| response := new(TagResponse) | ||
| if err := json.Unmarshal(body, response); err != nil { | ||
| return nil, fmt.Errorf("unexpected image tags response: %s", body) | ||
| } | ||
|
|
||
| return response, nil | ||
| } | ||
|
|
||
| func getAnonymousToken(ctx context.Context, client *http.Client) (string, error) { | ||
| req, err := http.NewRequest(http.MethodGet, loginURL, nil) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| req = req.WithContext(ctx) | ||
|
|
||
| resp, err := client.Do(req) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| defer func() { _ = resp.Body.Close() }() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return "", errors.New(string(body)) | ||
| } | ||
|
|
||
| response := new(AuthResponse) | ||
| if err := json.Unmarshal(body, response); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return response.Token, nil | ||
| } |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main client functionality in ecrpublic.go lacks test coverage. While path_test.go tests IsHost() and RepoImageFromPath(), there are no tests for the core functions Tags(), doRequest(), and getAnonymousToken(). Other client implementations like docker and ghcr include comprehensive tests for these core functions. Consider adding tests similar to those in pkg/client/docker/docker_test.go that use httptest to mock API responses.
| resp, err := c.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get %q image: %s", c.Name(), err) | ||
| } | ||
| defer func() { _ = resp.Body.Close() }() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| response := new(TagResponse) | ||
| if err := json.Unmarshal(body, response); err != nil { | ||
| return nil, fmt.Errorf("unexpected image tags response: %s", body) | ||
| } |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doRequest function doesn't check the HTTP status code before attempting to unmarshal the response body. If the API returns an error status (e.g., 404, 401, 500), the function will try to unmarshal the error response as a TagResponse, which will likely fail with a confusing error message. Consider adding a status code check similar to getAnonymousToken at line 142:
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get tags, status code: %d, body: %s", resp.StatusCode, body)
}| path string | ||
| expRepo, expImage string | ||
| }{ | ||
| "single image should return registry and image": { |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test name says "single image should return registry and image" but the expected expImage is empty. This is confusing and the test name doesn't match the expectation. Consider renaming to something like "single segment path should return repo with empty image" to better reflect what's being tested.
| "single image should return registry and image": { | |
| "single segment path should return repo with empty image": { |
| // Always get a token for ECR Public | ||
| token, err := getAnonymousToken(ctx, c.Client) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get anonymous token: %s", err) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message formatting is inconsistent with the error returned from getAnonymousToken. The error should use %w instead of %s to properly wrap the error, allowing error unwrapping with errors.Is() and errors.As(). Change to: fmt.Errorf("failed to get anonymous token: %w", err)
| return nil, fmt.Errorf("failed to get anonymous token: %s", err) | |
| return nil, fmt.Errorf("failed to get anonymous token: %w", err) |
|
|
||
| resp, err := c.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get %q image: %s", c.Name(), err) |
Copilot
AI
Dec 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, this error should use %w instead of %s to properly wrap the error. Change to: fmt.Errorf("failed to get %q image: %w", c.Name(), err)
| return nil, fmt.Errorf("failed to get %q image: %s", c.Name(), err) | |
| return nil, fmt.Errorf("failed to get %q image: %w", c.Name(), err) |

Following PR adds support for images hosted in the AWS public registry, aims to solve the following issue. WIP