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  }