// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package api import ( "context" "encoding/base64" "fmt" "net/url" "regexp" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/aws/aws-sdk-go-v2/service/ecrpublic" "github.com/sirupsen/logrus" "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cache" ) const ( proxyEndpointScheme = "https://" programName = "docker-credential-ecr-login" ecrPublicName = "public.ecr.aws" ecrPublicEndpoint = proxyEndpointScheme + ecrPublicName ) var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?$`) type Service string const ( ServiceECR Service = "ecr" ServiceECRPublic Service = "ecr-public" ) // Registry in ECR type Registry struct { Service Service ID string FIPS bool Region string } // ExtractRegistry returns the ECR registry behind a given service endpoint func ExtractRegistry(input string) (*Registry, error) { if strings.HasPrefix(input, proxyEndpointScheme) { input = strings.TrimPrefix(input, proxyEndpointScheme) } serverURL, err := url.Parse(proxyEndpointScheme + input) if err != nil { return nil, err } if serverURL.Hostname() == ecrPublicName { return &Registry{ Service: ServiceECRPublic, }, nil } matches := ecrPattern.FindStringSubmatch(serverURL.Hostname()) if len(matches) == 0 { return nil, fmt.Errorf(programName + " can only be used with Amazon Elastic Container Registry.") } else if len(matches) < 3 { return nil, fmt.Errorf("%q is not a valid repository URI for Amazon Elastic Container Registry.", input) } return &Registry{ Service: ServiceECR, ID: matches[1], FIPS: matches[2] == "-fips", Region: matches[3], }, nil } // Client used for calling ECR service type Client interface { GetCredentials(serverURL string) (*Auth, error) GetCredentialsByRegistryID(registryID string) (*Auth, error) ListCredentials() ([]*Auth, error) } // Auth credentials returned by ECR service to allow docker login type Auth struct { ProxyEndpoint string Username string Password string } type defaultClient struct { ecrClient ECRAPI ecrPublicClient ECRPublicAPI credentialCache cache.CredentialsCache } type ECRAPI interface { GetAuthorizationToken(context.Context, *ecr.GetAuthorizationTokenInput, ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) } type ECRPublicAPI interface { GetAuthorizationToken(context.Context, *ecrpublic.GetAuthorizationTokenInput, ...func(*ecrpublic.Options)) (*ecrpublic.GetAuthorizationTokenOutput, error) } // GetCredentials returns username, password, and proxyEndpoint func (c *defaultClient) GetCredentials(serverURL string) (*Auth, error) { registry, err := ExtractRegistry(serverURL) if err != nil { return nil, err } logrus. WithField("service", registry.Service). WithField("registry", registry.ID). WithField("region", registry.Region). WithField("serverURL", serverURL). Debug("Retrieving credentials") switch registry.Service { case ServiceECR: return c.GetCredentialsByRegistryID(registry.ID) case ServiceECRPublic: return c.GetPublicCredentials() } return nil, fmt.Errorf("unknown service %q", registry.Service) } // GetCredentialsByRegistryID returns username, password, and proxyEndpoint func (c *defaultClient) GetCredentialsByRegistryID(registryID string) (*Auth, error) { cachedEntry := c.credentialCache.Get(registryID) if cachedEntry != nil { if cachedEntry.IsValid(time.Now()) { logrus.WithField("registry", registryID).Debug("Using cached token") return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint) } logrus. WithField("requestedAt", cachedEntry.RequestedAt). WithField("expiresAt", cachedEntry.ExpiresAt). Debug("Cached token is no longer valid") } auth, err := c.getAuthorizationToken(registryID) // if we have a cached token, fall back to avoid failing the request. This may result an expired token // being returned, but if there is a 500 or timeout from the service side, we'd like to attempt to re-use an // old token. We invalidate tokens prior to their expiration date to help mitigate this scenario. if err != nil && cachedEntry != nil { logrus.WithError(err).Info("Got error fetching authorization token. Falling back to cached token.") return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint) } return auth, err } func (c *defaultClient) GetPublicCredentials() (*Auth, error) { cachedEntry := c.credentialCache.GetPublic() if cachedEntry != nil { if cachedEntry.IsValid(time.Now()) { logrus.WithField("registry", ecrPublicName).Debug("Using cached token") return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint) } logrus. WithField("requestedAt", cachedEntry.RequestedAt). WithField("expiresAt", cachedEntry.ExpiresAt). Debug("Cached token is no longer valid") } auth, err := c.getPublicAuthorizationToken() // if we have a cached token, fall back to avoid failing the request. This may result an expired token // being returned, but if there is a 500 or timeout from the service side, we'd like to attempt to re-use an // old token. We invalidate tokens prior to their expiration date to help mitigate this scenario. if err != nil && cachedEntry != nil { logrus.WithError(err).Info("Got error fetching authorization token. Falling back to cached token.") return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint) } return auth, err } func (c *defaultClient) ListCredentials() ([]*Auth, error) { // prime the cache with default authorization tokens _, err := c.GetCredentialsByRegistryID("") if err != nil { logrus.WithError(err).Debug("couldn't get authorization token for default registry") } _, err = c.GetPublicCredentials() if err != nil { logrus.WithError(err).Debug("couldn't get authorization token for public registry") } auths := make([]*Auth, 0) for _, authEntry := range c.credentialCache.List() { auth, err := extractToken(authEntry.AuthorizationToken, authEntry.ProxyEndpoint) if err != nil { logrus.WithError(err).Debug("Could not extract token") } else { auths = append(auths, auth) } } return auths, nil } func (c *defaultClient) getAuthorizationToken(registryID string) (*Auth, error) { var input *ecr.GetAuthorizationTokenInput if registryID == "" { logrus.Debug("Calling ECR.GetAuthorizationToken for default registry") input = &ecr.GetAuthorizationTokenInput{} } else { logrus.WithField("registry", registryID).Debug("Calling ECR.GetAuthorizationToken") input = &ecr.GetAuthorizationTokenInput{ RegistryIds: []string{registryID}, } } output, err := c.ecrClient.GetAuthorizationToken(context.TODO(), input) if err != nil || output == nil { if err == nil { if registryID == "" { err = fmt.Errorf("missing AuthorizationData in ECR response for default registry") } else { err = fmt.Errorf("missing AuthorizationData in ECR response for %s", registryID) } } return nil, fmt.Errorf("ecr: Failed to get authorization token: %w", err) } for _, authData := range output.AuthorizationData { if authData.ProxyEndpoint != nil && authData.AuthorizationToken != nil { authEntry := cache.AuthEntry{ AuthorizationToken: aws.ToString(authData.AuthorizationToken), RequestedAt: time.Now(), ExpiresAt: aws.ToTime(authData.ExpiresAt), ProxyEndpoint: aws.ToString(authData.ProxyEndpoint), Service: cache.ServiceECR, } registry, err := ExtractRegistry(authEntry.ProxyEndpoint) if err != nil { return nil, fmt.Errorf("Invalid ProxyEndpoint returned by ECR: %s", authEntry.ProxyEndpoint) } auth, err := extractToken(authEntry.AuthorizationToken, authEntry.ProxyEndpoint) if err != nil { return nil, err } c.credentialCache.Set(registry.ID, &authEntry) return auth, nil } } if registryID == "" { return nil, fmt.Errorf("No AuthorizationToken found for default registry") } return nil, fmt.Errorf("No AuthorizationToken found for %s", registryID) } func (c *defaultClient) getPublicAuthorizationToken() (*Auth, error) { var input *ecrpublic.GetAuthorizationTokenInput output, err := c.ecrPublicClient.GetAuthorizationToken(context.TODO(), input) if err != nil { return nil, fmt.Errorf("ecr: failed to get authorization token: %w", err) } if output == nil || output.AuthorizationData == nil { return nil, fmt.Errorf("ecr: missing AuthorizationData in ECR Public response") } authData := output.AuthorizationData token, err := extractToken(aws.ToString(authData.AuthorizationToken), ecrPublicEndpoint) if err != nil { return nil, err } authEntry := cache.AuthEntry{ AuthorizationToken: aws.ToString(authData.AuthorizationToken), RequestedAt: time.Now(), ExpiresAt: aws.ToTime(authData.ExpiresAt), ProxyEndpoint: ecrPublicEndpoint, Service: cache.ServiceECRPublic, } c.credentialCache.Set(ecrPublicName, &authEntry) return token, nil } func extractToken(token string, proxyEndpoint string) (*Auth, error) { decodedToken, err := base64.StdEncoding.DecodeString(token) if err != nil { return nil, fmt.Errorf("invalid token: %w", err) } parts := strings.SplitN(string(decodedToken), ":", 2) if len(parts) < 2 { return nil, fmt.Errorf("invalid token: expected two parts, got %d", len(parts)) } return &Auth{ Username: parts[0], Password: parts[1], ProxyEndpoint: proxyEndpoint, }, nil }