github.com/vdemeester/k8s-pkg-credentialprovider@v1.22.4/aws/aws_credentials.go (about) 1 /* 2 Copyright 2014 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package credentials 18 19 import ( 20 "encoding/base64" 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "net/url" 25 "os" 26 "regexp" 27 "strings" 28 "sync" 29 "time" 30 31 "github.com/aws/aws-sdk-go/aws" 32 "github.com/aws/aws-sdk-go/aws/request" 33 "github.com/aws/aws-sdk-go/aws/session" 34 "github.com/aws/aws-sdk-go/service/ecr" 35 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/client-go/tools/cache" 38 "k8s.io/component-base/version" 39 "k8s.io/klog/v2" 40 "github.com/vdemeester/k8s-pkg-credentialprovider" 41 ) 42 43 var ( 44 ecrPattern = regexp.MustCompile(`^(\d{12})\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?$`) 45 once sync.Once 46 isEC2 bool 47 ) 48 49 // init registers a credential provider for each registryURLTemplate and creates 50 // an ECR token getter factory with a new cache to store token getters 51 func init() { 52 credentialprovider.RegisterCredentialProvider("amazon-ecr", 53 newECRProvider(&ecrTokenGetterFactory{cache: make(map[string]tokenGetter)}, 54 ec2ValidationImpl, 55 )) 56 } 57 58 // ecrProvider is a DockerConfigProvider that gets and refreshes tokens 59 // from AWS to access ECR. 60 type ecrProvider struct { 61 cache cache.Store 62 getterFactory tokenGetterFactory 63 isEC2 ec2ValidationFunc 64 } 65 66 var _ credentialprovider.DockerConfigProvider = &ecrProvider{} 67 68 func newECRProvider(getterFactory tokenGetterFactory, isEC2 ec2ValidationFunc) *ecrProvider { 69 return &ecrProvider{ 70 cache: cache.NewExpirationStore(stringKeyFunc, &ecrExpirationPolicy{}), 71 getterFactory: getterFactory, 72 isEC2: isEC2, 73 } 74 } 75 76 // Enabled implements DockerConfigProvider.Enabled. 77 func (p *ecrProvider) Enabled() bool { 78 return true 79 } 80 81 type ec2ValidationFunc func() bool 82 83 // ec2ValidationImpl returns true if we detect 84 // an EC2 vm based on checking for the EC2 system UUID, the asset tag (for nitro 85 // instances), or instance credentials if the UUID is not present. 86 // Ref: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html 87 func ec2ValidationImpl() bool { 88 return tryValidateEC2UUID() || tryValidateEC2Creds() 89 } 90 91 func tryValidateEC2UUID() bool { 92 hypervisor_uuid := "/sys/hypervisor/uuid" 93 product_uuid := "/sys/devices/virtual/dmi/id/product_uuid" 94 asset_tag := "/sys/devices/virtual/dmi/id/board_asset_tag" 95 96 if _, err := os.Stat(hypervisor_uuid); err == nil { 97 b, err := ioutil.ReadFile(hypervisor_uuid) 98 if err != nil { 99 klog.Errorf("error checking if this is an EC2 instance: %v", err) 100 } else if strings.HasPrefix(string(b), "EC2") || strings.HasPrefix(string(b), "ec2") { 101 klog.V(5).Infof("found 'ec2' in uuid %v from %v, enabling legacy AWS credential provider", string(b), hypervisor_uuid) 102 return true 103 } 104 } 105 106 if _, err := os.Stat(product_uuid); err == nil { 107 b, err := ioutil.ReadFile(product_uuid) 108 if err != nil { 109 klog.Errorf("error checking if this is an EC2 instance: %v", err) 110 } else if strings.HasPrefix(string(b), "EC2") || strings.HasPrefix(string(b), "ec2") { 111 klog.V(5).Infof("found 'ec2' in uuid %v from %v, enabling legacy AWS credential provider", string(b), product_uuid) 112 return true 113 } 114 } 115 116 if _, err := os.Stat(asset_tag); err == nil { 117 b, err := ioutil.ReadFile(asset_tag) 118 s := strings.TrimSpace(string(b)) 119 if err != nil { 120 klog.Errorf("error checking if this is an EC2 instance: %v", err) 121 } else if strings.HasPrefix(s, "i-") && len(s) == 19 { 122 // Instance ID's are 19 characters plus newline 123 klog.V(5).Infof("found instance ID in %v from %v, enabling legacy AWS credential provider", string(b), asset_tag) 124 return true 125 } 126 } 127 return false 128 } 129 130 func tryValidateEC2Creds() bool { 131 sess, err := session.NewSessionWithOptions(session.Options{ 132 SharedConfigState: session.SharedConfigEnable, 133 }) 134 if err != nil { 135 klog.Errorf("while validating AWS credentials %v", err) 136 return false 137 } 138 if _, err := sess.Config.Credentials.Get(); err != nil { 139 klog.Errorf("while getting AWS credentials %v", err) 140 return false 141 } 142 klog.V(5).Infof("found aws credentials, enabling legacy AWS credential provider") 143 return true 144 } 145 146 // Provide returns a DockerConfig with credentials from the cache if they are 147 // found, or from ECR 148 func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig { 149 parsed, err := parseRepoURL(image) 150 if err != nil { 151 return credentialprovider.DockerConfig{} 152 } 153 154 // To prevent the AWS SDK from causing latency on non-aws platforms, only test if we are on 155 // EC2 or have access to credentials once. Attempt to do it without network calls by checking 156 // for certain EC2-specific files. Otherwise, we ask the SDK to initialize a session to see if 157 // credentials are available. On non-aws platforms, especially when a metadata endpoint is blocked, 158 // this has been shown to cause 20 seconds of latency due to SDK retries 159 // (see https://github.com/kubernetes/kubernetes/issues/92162) 160 once.Do(func() { 161 isEC2 = p.isEC2() 162 }) 163 164 if !isEC2 { 165 return credentialprovider.DockerConfig{} 166 } 167 168 if cfg, exists := p.getFromCache(parsed); exists { 169 klog.V(3).Infof("Got ECR credentials from cache for %s", parsed.registry) 170 return cfg 171 } 172 klog.V(3).Info("unable to get ECR credentials from cache, checking ECR API") 173 174 cfg, err := p.getFromECR(parsed) 175 if err != nil { 176 klog.Errorf("error getting credentials from ECR for %s %v", parsed.registry, err) 177 return credentialprovider.DockerConfig{} 178 } 179 klog.V(3).Infof("Got ECR credentials from ECR API for %s", parsed.registry) 180 return cfg 181 } 182 183 // getFromCache attempts to get credentials from the cache 184 func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.DockerConfig, bool) { 185 cfg := credentialprovider.DockerConfig{} 186 187 obj, exists, err := p.cache.GetByKey(parsed.registry) 188 if err != nil { 189 klog.Errorf("error getting ECR credentials from cache: %v", err) 190 return cfg, false 191 } 192 193 if !exists { 194 return cfg, false 195 } 196 197 entry := obj.(*cacheEntry) 198 cfg[entry.registry] = entry.credentials 199 return cfg, true 200 } 201 202 // getFromECR gets credentials from ECR since they are not in the cache 203 func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialprovider.DockerConfig, error) { 204 cfg := credentialprovider.DockerConfig{} 205 getter, err := p.getterFactory.GetTokenGetterForRegion(parsed.region) 206 if err != nil { 207 return cfg, err 208 } 209 params := &ecr.GetAuthorizationTokenInput{RegistryIds: []*string{aws.String(parsed.registryID)}} 210 output, err := getter.GetAuthorizationToken(params) 211 if err != nil { 212 return cfg, err 213 } 214 if output == nil { 215 return cfg, errors.New("authorization token is nil") 216 } 217 if len(output.AuthorizationData) == 0 { 218 return cfg, errors.New("authorization data from response is empty") 219 } 220 data := output.AuthorizationData[0] 221 if data.AuthorizationToken == nil { 222 return cfg, errors.New("authorization token in response is nil") 223 } 224 entry, err := makeCacheEntry(data, parsed.registry) 225 if err != nil { 226 return cfg, err 227 } 228 if err := p.cache.Add(entry); err != nil { 229 return cfg, err 230 } 231 cfg[entry.registry] = entry.credentials 232 return cfg, nil 233 } 234 235 type parsedURL struct { 236 registryID string 237 region string 238 registry string 239 } 240 241 // parseRepoURL parses and splits the registry URL into the registry ID, 242 // region, and registry. 243 // <registryID>.dkr.ecr(-fips).<region>.amazonaws.com(.cn) 244 func parseRepoURL(image string) (*parsedURL, error) { 245 parsed, err := url.Parse("https://" + image) 246 if err != nil { 247 return nil, fmt.Errorf("error parsing image %s %v", image, err) 248 } 249 splitURL := ecrPattern.FindStringSubmatch(parsed.Hostname()) 250 if len(splitURL) == 0 { 251 return nil, fmt.Errorf("%s is not a valid ECR repository URL", parsed.Hostname()) 252 } 253 return &parsedURL{ 254 registryID: splitURL[1], 255 region: splitURL[3], 256 registry: parsed.Hostname(), 257 }, nil 258 } 259 260 // tokenGetter is for testing purposes 261 type tokenGetter interface { 262 GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) 263 } 264 265 // tokenGetterFactory is for testing purposes 266 type tokenGetterFactory interface { 267 GetTokenGetterForRegion(string) (tokenGetter, error) 268 } 269 270 // ecrTokenGetterFactory stores a token getter per region 271 type ecrTokenGetterFactory struct { 272 cache map[string]tokenGetter 273 mutex sync.Mutex 274 } 275 276 // awsHandlerLogger is a handler that logs all AWS SDK requests 277 // Copied from pkg/cloudprovider/providers/aws/log_handler.go 278 func awsHandlerLogger(req *request.Request) { 279 service := req.ClientInfo.ServiceName 280 region := req.Config.Region 281 282 name := "?" 283 if req.Operation != nil { 284 name = req.Operation.Name 285 } 286 287 klog.V(3).Infof("AWS request: %s:%s in %s", service, name, *region) 288 } 289 290 func newECRTokenGetter(region string) (tokenGetter, error) { 291 sess, err := session.NewSessionWithOptions(session.Options{ 292 Config: aws.Config{Region: aws.String(region)}, 293 SharedConfigState: session.SharedConfigEnable, 294 }) 295 if err != nil { 296 return nil, err 297 } 298 getter := &ecrTokenGetter{svc: ecr.New(sess)} 299 getter.svc.Handlers.Build.PushFrontNamed(request.NamedHandler{ 300 Name: "k8s/user-agent", 301 Fn: request.MakeAddToUserAgentHandler("kubernetes", version.Get().String()), 302 }) 303 getter.svc.Handlers.Sign.PushFrontNamed(request.NamedHandler{ 304 Name: "k8s/logger", 305 Fn: awsHandlerLogger, 306 }) 307 return getter, nil 308 } 309 310 // GetTokenGetterForRegion gets the token getter for the requested region. If it 311 // doesn't exist, it creates a new ECR token getter 312 func (f *ecrTokenGetterFactory) GetTokenGetterForRegion(region string) (tokenGetter, error) { 313 f.mutex.Lock() 314 defer f.mutex.Unlock() 315 316 if getter, ok := f.cache[region]; ok { 317 return getter, nil 318 } 319 getter, err := newECRTokenGetter(region) 320 if err != nil { 321 return nil, fmt.Errorf("unable to create token getter for region %v %v", region, err) 322 } 323 f.cache[region] = getter 324 return getter, nil 325 } 326 327 // The canonical implementation 328 type ecrTokenGetter struct { 329 svc *ecr.ECR 330 } 331 332 // GetAuthorizationToken gets the ECR authorization token using the ECR API 333 func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) { 334 return p.svc.GetAuthorizationToken(input) 335 } 336 337 type cacheEntry struct { 338 expiresAt time.Time 339 credentials credentialprovider.DockerConfigEntry 340 registry string 341 } 342 343 // makeCacheEntry decodes the ECR authorization entry and re-packages it into a 344 // cacheEntry. 345 func makeCacheEntry(data *ecr.AuthorizationData, registry string) (*cacheEntry, error) { 346 decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken)) 347 if err != nil { 348 return nil, fmt.Errorf("error decoding ECR authorization token: %v", err) 349 } 350 parts := strings.SplitN(string(decodedToken), ":", 2) 351 if len(parts) < 2 { 352 return nil, errors.New("error getting username and password from authorization token") 353 } 354 creds := credentialprovider.DockerConfigEntry{ 355 Username: parts[0], 356 Password: parts[1], 357 Email: "not@val.id", // ECR doesn't care and Docker is about to obsolete it 358 } 359 if data.ExpiresAt == nil { 360 return nil, errors.New("authorization data expiresAt is nil") 361 } 362 return &cacheEntry{ 363 expiresAt: data.ExpiresAt.Add(-1 * wait.Jitter(30*time.Minute, 0.2)), 364 credentials: creds, 365 registry: registry, 366 }, nil 367 } 368 369 // ecrExpirationPolicy implements ExpirationPolicy from client-go. 370 type ecrExpirationPolicy struct{} 371 372 // stringKeyFunc returns the cache key as a string 373 func stringKeyFunc(obj interface{}) (string, error) { 374 key := obj.(*cacheEntry).registry 375 return key, nil 376 } 377 378 // IsExpired checks if the ECR credentials are expired. 379 func (p *ecrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool { 380 return time.Now().After(entry.Obj.(*cacheEntry).expiresAt) 381 }