github.com/buildtool/build-tools@v0.2.29-0.20240322150259-6a1d0a553c23/pkg/registry/ecr.go (about)

     1  // MIT License
     2  //
     3  // Copyright (c) 2018 buildtool
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  
    23  package registry
    24  
    25  import (
    26  	"context"
    27  	"encoding/base64"
    28  	"encoding/json"
    29  	"errors"
    30  	"fmt"
    31  	"regexp"
    32  	"strings"
    33  
    34  	"github.com/apex/log"
    35  	"github.com/aws/aws-sdk-go-v2/aws"
    36  	"github.com/aws/aws-sdk-go-v2/config"
    37  	"github.com/aws/aws-sdk-go-v2/service/ecr"
    38  	"github.com/aws/aws-sdk-go-v2/service/ecr/types"
    39  	"github.com/aws/aws-sdk-go-v2/service/sts"
    40  	"github.com/docker/docker/api/types/registry"
    41  
    42  	"github.com/buildtool/build-tools/pkg/docker"
    43  )
    44  
    45  type ECRClient interface {
    46  	GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)
    47  	DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error)
    48  	CreateRepository(ctx context.Context, params *ecr.CreateRepositoryInput, optFns ...func(*ecr.Options)) (*ecr.CreateRepositoryOutput, error)
    49  	PutLifecyclePolicy(ctx context.Context, params *ecr.PutLifecyclePolicyInput, optFns ...func(*ecr.Options)) (*ecr.PutLifecyclePolicyOutput, error)
    50  }
    51  
    52  type STSClient interface {
    53  	GetCallerIdentity(ctx context.Context, params *sts.GetCallerIdentityInput, optFns ...func(*sts.Options)) (*sts.GetCallerIdentityOutput, error)
    54  }
    55  
    56  type ECR struct {
    57  	dockerRegistry `yaml:"-"`
    58  	Url            string `yaml:"url" env:"ECR_URL"`
    59  	Region         string `yaml:"region,omitempty" env:"ECR_REGION"`
    60  	username       string
    61  	password       string
    62  	ecrSvc         ECRClient
    63  	stsSvc         STSClient
    64  	registryId     *string
    65  }
    66  
    67  var _ Registry = &ECR{}
    68  
    69  func (r *ECR) Name() string {
    70  	return "ECR"
    71  }
    72  
    73  func (r *ECR) Configured() bool {
    74  	if len(r.Url) > 0 {
    75  		sess, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(r.Region))
    76  		if err != nil {
    77  			return false
    78  		}
    79  		r.ecrSvc = ecr.NewFromConfig(sess)
    80  		r.stsSvc = sts.NewFromConfig(sess)
    81  		registryId, err := r.registry()
    82  		if err != nil {
    83  			return false
    84  		}
    85  		r.registryId = registryId
    86  		return true
    87  	}
    88  	return false
    89  }
    90  
    91  func (r *ECR) region() *string {
    92  	if r.Region == "" {
    93  		regex := regexp.MustCompile(`.*\.dkr\.ecr.(.*)\.amazonaws\.com`)
    94  		if submatch := regex.FindStringSubmatch(r.Url); len(submatch) == 2 {
    95  			r.Region = submatch[1]
    96  		}
    97  	}
    98  	return &r.Region
    99  }
   100  
   101  func (r *ECR) registry() (*string, error) {
   102  	regex := regexp.MustCompile(`(.*)\.dkr\.ecr..*\.amazonaws\.com`)
   103  	if submatch := regex.FindStringSubmatch(r.Url); len(submatch) == 2 {
   104  		return &submatch[1], nil
   105  	}
   106  	return nil, fmt.Errorf("failed to extract registryid from string %s", r.Url)
   107  }
   108  
   109  func (r *ECR) Login(client docker.Client) error {
   110  	input := &ecr.GetAuthorizationTokenInput{}
   111  
   112  	result, err := r.ecrSvc.GetAuthorizationToken(context.Background(), input)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	decoded, err := base64.StdEncoding.DecodeString(*result.AuthorizationData[0].AuthorizationToken)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	parts := strings.Split(string(decoded), ":")
   122  	r.username = parts[0]
   123  	r.password = parts[1]
   124  
   125  	if ok, err := client.RegistryLogin(context.Background(), registry.AuthConfig{Username: r.username, Password: r.password, ServerAddress: r.Url}); err == nil {
   126  		log.Debugf("%s\n", ok.Status)
   127  		return nil
   128  	} else {
   129  		return err
   130  	}
   131  }
   132  
   133  func (r *ECR) GetAuthConfig() registry.AuthConfig {
   134  	return registry.AuthConfig{Username: r.username, Password: r.password}
   135  }
   136  
   137  func (r *ECR) GetAuthInfo() string {
   138  	authBytes, _ := json.Marshal(r.GetAuthConfig())
   139  	return base64.URLEncoding.EncodeToString(authBytes)
   140  }
   141  
   142  func (r ECR) RegistryUrl() string {
   143  	return r.Url
   144  }
   145  
   146  func (r ECR) Create(repository string) error {
   147  	identity, err := r.stsSvc.GetCallerIdentity(context.Background(), &sts.GetCallerIdentityInput{})
   148  	if err != nil {
   149  		return err
   150  	}
   151  	if *identity.Account != *r.registryId {
   152  		return fmt.Errorf("account mismatch, logged in at '%s' got '%s' from repository url %s", *identity.Account, *r.registryId, r.Url)
   153  	}
   154  	if _, err := r.ecrSvc.DescribeRepositories(context.Background(), &ecr.DescribeRepositoriesInput{
   155  		RegistryId:      r.registryId,
   156  		RepositoryNames: []string{repository},
   157  	}); err != nil {
   158  		var aerr *types.RepositoryNotFoundException
   159  		if !errors.As(err, &aerr) {
   160  			return err
   161  		}
   162  		input := &ecr.CreateRepositoryInput{
   163  			RepositoryName: aws.String(repository),
   164  		}
   165  
   166  		if _, err := r.ecrSvc.CreateRepository(context.Background(), input); err != nil {
   167  			return err
   168  		} else {
   169  			policyText := `{"rules":[{"rulePriority":10,"description":"Only keep 20 images","selection":{"tagStatus":"untagged","countType":"imageCountMoreThan","countNumber":20},"action":{"type":"expire"}}]}`
   170  			if _, err := r.ecrSvc.PutLifecyclePolicy(context.Background(), &ecr.PutLifecyclePolicyInput{LifecyclePolicyText: &policyText, RepositoryName: &repository}); err != nil {
   171  				return err
   172  			}
   173  			return nil
   174  		}
   175  	}
   176  	return nil
   177  }