-
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?
Changes from all commits
c8be71c
3bd6b18
eaf2aa7
16e18e7
d33d50c
5a696ab
0f7b620
370b1c1
3944e3f
19331eb
b36e218
dc3ffaf
df64020
9d6bfea
3e054f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package ecrpublic | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "time" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
|
|
||
| retryablehttp "github.com/hashicorp/go-retryablehttp" | ||
| "github.com/jetstack/version-checker/pkg/api" | ||
| "github.com/jetstack/version-checker/pkg/client/util" | ||
| ) | ||
|
|
||
| const ( | ||
| ecrPublicLookupURL = "https://public.ecr.aws/v2/%s/%s/tags/list" | ||
| loginURL = "https://public.ecr.aws/token/?service=ecr-public" | ||
| ) | ||
|
|
||
| type Options struct { | ||
| Username string | ||
| Password string | ||
| Transporter http.RoundTripper | ||
| } | ||
|
|
||
| type Client struct { | ||
| *http.Client | ||
| Options | ||
| } | ||
|
|
||
| var _ api.ImageClient = (*Client)(nil) | ||
| func New(opts Options, log *logrus.Entry) (*Client, error) { | ||
| retryclient := retryablehttp.NewClient() | ||
| if opts.Transporter != nil { | ||
| retryclient.HTTPClient.Transport = opts.Transporter | ||
| } | ||
| retryclient.HTTPClient.Timeout = 10 * time.Second | ||
| retryclient.RetryMax = 10 | ||
| retryclient.RetryWaitMax = 10 * time.Minute | ||
| retryclient.RetryWaitMin = 1 * time.Second | ||
| // This custom backoff will fail requests that have a max wait of the RetryWaitMax | ||
| retryclient.Backoff = util.HTTPBackOff | ||
| retryclient.Logger = log.WithField("client", "ecrpublic") | ||
| client := retryclient.StandardClient() | ||
|
|
||
| return &Client{ | ||
| Options: opts, | ||
| Client: client, | ||
| }, nil | ||
| } | ||
|
|
||
| func (c *Client) Name() string { | ||
| return "ecrpublic" | ||
| } | ||
|
|
||
| func (c *Client) Tags(ctx context.Context, _, repo, image string) ([]api.ImageTag, error) { | ||
| var url string | ||
| if image == "" { | ||
| url = fmt.Sprintf("https://public.ecr.aws/v2/%s/tags/list", repo) | ||
| } else { | ||
| 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: %w", 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: %w", 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) | ||
| } | ||
|
Comment on lines
+110
to
+124
|
||
|
|
||
| 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 | ||
| } | ||
|
Comment on lines
+60
to
+158
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package ecrpublic | ||
|
|
||
| import ( | ||
| "regexp" | ||
| "strings" | ||
| ) | ||
|
|
||
| var ( | ||
| ecrPublicPattern = regexp.MustCompile(`^public\.ecr\.aws$`) | ||
| ) | ||
|
|
||
| func (c *Client) IsHost(host string) bool { | ||
| return ecrPublicPattern.MatchString(host) | ||
| } | ||
|
|
||
| func (c *Client) RepoImageFromPath(path string) (string, string) { | ||
| parts := strings.Split(path, "/") | ||
|
|
||
| return parts[0], strings.Join(parts[1:], "/") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package ecrpublic | ||
|
|
||
| import "testing" | ||
|
|
||
| func TestIsHost(t *testing.T) { | ||
| tests := map[string]struct { | ||
| host string | ||
| expIs bool | ||
| }{ | ||
| "an empty host should be false": { | ||
| host: "", | ||
| expIs: false, | ||
| }, | ||
| "random string should be false": { | ||
| host: "foobar", | ||
| expIs: false, | ||
| }, | ||
| "path with two segments should be false": { | ||
| host: "joshvanl/version-checker", | ||
| expIs: false, | ||
| }, | ||
| "path with three segments should be false": { | ||
| host: "jetstack/joshvanl/version-checker", | ||
| expIs: false, | ||
| }, | ||
| "random string with dots should be false": { | ||
| host: "foobar.foo", | ||
| expIs: false, | ||
| }, | ||
| "docker.io should be false": { | ||
| host: "docker.io", | ||
| expIs: false, | ||
| }, | ||
| "docker.com should be false": { | ||
| host: "docker.com", | ||
| expIs: false, | ||
| }, | ||
| "just public.ecr.aws should be true": { | ||
| host: "public.ecr.aws", | ||
| expIs: true, | ||
| }, | ||
| "public.ecr.aws.foo should be false": { | ||
| host: "public.ecr.aws.foo", | ||
| expIs: false, | ||
| }, | ||
| "foo.public.ecr.aws should be false": { | ||
| host: "foo.public.ecr.aws", | ||
| expIs: false, | ||
| }, | ||
| } | ||
|
|
||
| handler := new(Client) | ||
| for name, test := range tests { | ||
| t.Run(name, func(t *testing.T) { | ||
| if isHost := handler.IsHost(test.host); isHost != test.expIs { | ||
| t.Errorf("%s: unexpected IsHost, exp=%t got=%t", | ||
| test.host, test.expIs, isHost) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestRepoImageFromPath(t *testing.T) { | ||
| tests := map[string]struct { | ||
| path string | ||
| expRepo, expImage string | ||
| }{ | ||
| "single segment path should return repo with empty image": { | ||
| path: "nginx", | ||
| expRepo: "nginx", | ||
| expImage: "", | ||
| }, | ||
| "two segments to path should return registry and repo": { | ||
| path: "eks-distro/kubernetes", | ||
| expRepo: "eks-distro", | ||
| expImage: "kubernetes", | ||
| }, | ||
| "three segments to path should return registry and combined repo": { | ||
| path: "eks-distro/kubernetes/kube-proxy", | ||
| expRepo: "eks-distro", | ||
| expImage: "kubernetes/kube-proxy", | ||
| }, | ||
| } | ||
|
|
||
| handler := new(Client) | ||
| for name, test := range tests { | ||
| t.Run(name, func(t *testing.T) { | ||
| repo, image := handler.RepoImageFromPath(test.path) | ||
| if repo != test.expRepo || image != test.expImage { | ||
| t.Errorf("%s: unexpected repo/image, exp=%s/%s got=%s/%s", | ||
| test.path, test.expRepo, test.expImage, repo, image) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package ecrpublic | ||
|
|
||
| type AuthResponse struct { | ||
| Token string `json:"token"` | ||
| } | ||
|
|
||
| type TagResponse struct { | ||
| Next string `json:"next"` | ||
| Name string `json:"name"` | ||
| Tags []string `json:"tags"` | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.