// Copyright 2015-2017 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 ecr import ( "strings" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/config" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/ecr" "github.com/aws/aws-sdk-go/service/ecr/ecriface" login "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) const ( CacheDir = "~/.ecs" ) // ProcessImageDetails callback function for describe images type ProcessImageDetails func(images []*ecr.ImageDetail) error // ProcessRepositories callback function for describe repositories type ProcessRepositories func(repositories []*string) error // Client ECR interface type Client interface { GetAuthorizationToken(registryURI string) (*Auth, error) GetAuthorizationTokenByID(registryID string) (*Auth, error) CreateRepository(repositoryName string) (string, error) RepositoryExists(repositoryName string) bool GetImages(repositoryNames []*string, tagStatus string, registryID string, processFn ProcessImageDetails) error } // ecrClient implements Client type ecrClient struct { client ecriface.ECRAPI loginClient login.Client config *config.CommandConfig auth *Auth } // NewClient Creates a new ECR client func NewClient(config *config.CommandConfig) Client { client := ecr.New(config.Session, config.Session.Config) client.Handlers.Build.PushBackNamed(clients.CustomUserAgentHandler()) loginClient := login.DefaultClientFactory{}.NewClientWithOptions(login.Options{ Session: config.Session, Config: config.Session.Config, CacheDir: CacheDir, }) return newClient(config, client, loginClient) } // NewFipsClient Creates a new ECR client that will communicate with a FIPS endpoint. func NewFipsClient(config *config.CommandConfig) (Client, error) { region := config.Region() resolver := endpoints.DefaultResolver() // The convention for FIPS endpoints is to add "-fips" to the official // service name; however this would result in an UnknownServiceError // being thrown. To allow the endpoint resolver to fallback to the // correct endpoint, we can set the ResolveUnknownService flag to true. endpoint, err := resolver.EndpointFor("ecr-fips", region, func(opts *endpoints.Options) { opts.ResolveUnknownService = true }) if err != nil { return nil, err } log.Debugf("Using FIPS endpoint: %+v", endpoint.URL) awsSession := config.Session awsConfig := &aws.Config{ Endpoint: aws.String(endpoint.URL), } client := ecr.New(awsSession, awsConfig) client.Handlers.Build.PushBackNamed(clients.CustomUserAgentHandler()) // NOTE: While there is a NewClientWithFipsEndpoint method on // login.DefaultClientFactory, this would instantiate a login client // with a different AWS session/config from that of the ecrClient. loginClient := login.DefaultClientFactory{}.NewClientWithOptions(login.Options{ Session: awsSession, Config: awsConfig, CacheDir: CacheDir, }) return newClient(config, client, loginClient), nil } func newClient(config *config.CommandConfig, client ecriface.ECRAPI, loginClient login.Client) Client { return &ecrClient{ config: config, client: client, loginClient: loginClient, } } // Auth keeps track of the ECR auth type Auth struct { ProxyEndpoint string Registry string Username string Password string } func (c *ecrClient) GetAuthorizationTokenByID(registryID string) (*Auth, error) { log.Debug("Getting authorization token...") auth, err := c.loginClient.GetCredentialsByRegistryID(registryID) if err != nil { return nil, errors.Wrap(err, "failed to serialize authorization token") } log.Debugf("Retrieved authorization token via endpoint: %+v", auth.ProxyEndpoint) return &Auth{ Username: auth.Username, Password: auth.Password, ProxyEndpoint: auth.ProxyEndpoint, Registry: strings.Replace(auth.ProxyEndpoint, "https://", "", -1), }, nil } func (c *ecrClient) GetAuthorizationToken(registryURI string) (*Auth, error) { log.Debug("Getting authorization token...") auth, err := c.loginClient.GetCredentials(registryURI) if err != nil { return nil, errors.Wrap(err, "failed to serialize authorization token") } log.Debugf("Retrieved authorization token via endpoint: %+v", auth.ProxyEndpoint) return &Auth{ Username: auth.Username, Password: auth.Password, ProxyEndpoint: auth.ProxyEndpoint, Registry: strings.Replace(auth.ProxyEndpoint, "https://", "", -1), }, nil } func (c *ecrClient) RepositoryExists(repositoryName string) bool { _, err := c.client.DescribeRepositories(&ecr.DescribeRepositoriesInput{RepositoryNames: []*string{&repositoryName}}) log.WithFields(log.Fields{ "repository": repositoryName, }).Debug("Check if repository exists") return err == nil } func (c *ecrClient) CreateRepository(repositoryName string) (string, error) { log.WithFields(log.Fields{ "repository": repositoryName, }).Info("Creating repository") resp, err := c.client.CreateRepository( &ecr.CreateRepositoryInput{RepositoryName: aws.String(repositoryName)}) if err != nil { return "", errors.Wrap(err, "unable to create repository") } if resp == nil || resp.Repository == nil { return "", errors.New("create repository response is empty") } log.Info("Repository created") return aws.StringValue(resp.Repository.RepositoryName), nil } func (c *ecrClient) GetImages(repositoryNames []*string, tagStatus string, registryID string, processFn ProcessImageDetails) error { log.Debug("Getting images from ECR...") pageNumber := 0 err := c.describeRepositories(repositoryNames, registryID, func(repositories []*string) error { for _, repository := range repositories { err := c.describeImages(aws.StringValue(repository), tagStatus, registryID, processFn, pageNumber) pageNumber++ if err != nil { return err } } return nil }) return err } func (c *ecrClient) describeRepositories(repositoryNames []*string, registryID string, outputFn ProcessRepositories) error { var outErr error input := &ecr.DescribeRepositoriesInput{} // Skip DescribeRepositories calls if repositoryNames are specified if len(repositoryNames) > 0 { err := outputFn(repositoryNames) return err } if registryID != "" { input.SetRegistryId(registryID) } err := c.client.DescribeRepositoriesPages(input, func(resp *ecr.DescribeRepositoriesOutput, lastPage bool) bool { repositoryNames = []*string{} for _, repository := range resp.Repositories { repositoryNames = append(repositoryNames, repository.RepositoryName) } if outErr = outputFn(repositoryNames); outErr != nil { return false } return !lastPage }) if err != nil { return err } return outErr } func (c *ecrClient) describeImages(repositoryName string, tagStatus string, registryID string, outputFn ProcessImageDetails, numOfCalls int) error { var outErr error filter := &ecr.DescribeImagesFilter{} if tagStatus != "" { filter.SetTagStatus(tagStatus) } input := &ecr.DescribeImagesInput{ RepositoryName: aws.String(repositoryName), Filter: filter, } if registryID != "" { input.SetRegistryId(registryID) } err := c.client.DescribeImagesPages(input, func(resp *ecr.DescribeImagesOutput, lastPage bool) bool { if outErr = outputFn(resp.ImageDetails); outErr != nil { return false } if numOfCalls > 50 { outErr = errors.New("please specify the repository name if you wish to see more") return false } return !lastPage }) if err != nil { return err } return outErr }