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 }