123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- /*
- Copyright 2014 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License 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 credentials
- import (
- "encoding/base64"
- "errors"
- "fmt"
- "net/url"
- "regexp"
- "strings"
- "sync"
- "time"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/ecr"
- "k8s.io/apimachinery/pkg/util/wait"
- "k8s.io/client-go/tools/cache"
- "k8s.io/component-base/version"
- "k8s.io/klog"
- "k8s.io/kubernetes/pkg/credentialprovider"
- )
- var ecrPattern = regexp.MustCompile(`^(\d{12})\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?$`)
- // init registers a credential provider for each registryURLTemplate and creates
- // an ECR token getter factory with a new cache to store token getters
- func init() {
- credentialprovider.RegisterCredentialProvider("amazon-ecr",
- newECRProvider(&ecrTokenGetterFactory{cache: make(map[string]tokenGetter)}))
- }
- // ecrProvider is a DockerConfigProvider that gets and refreshes tokens
- // from AWS to access ECR.
- type ecrProvider struct {
- cache cache.Store
- getterFactory tokenGetterFactory
- }
- var _ credentialprovider.DockerConfigProvider = &ecrProvider{}
- func newECRProvider(getterFactory tokenGetterFactory) *ecrProvider {
- return &ecrProvider{
- cache: cache.NewExpirationStore(stringKeyFunc, &ecrExpirationPolicy{}),
- getterFactory: getterFactory,
- }
- }
- // Enabled implements DockerConfigProvider.Enabled. Enabled is true if AWS
- // credentials are found.
- func (p *ecrProvider) Enabled() bool {
- sess, err := session.NewSessionWithOptions(session.Options{
- SharedConfigState: session.SharedConfigEnable,
- })
- if err != nil {
- klog.Errorf("while validating AWS credentials %v", err)
- return false
- }
- if _, err := sess.Config.Credentials.Get(); err != nil {
- klog.Errorf("while getting AWS credentials %v", err)
- return false
- }
- return true
- }
- // Provide returns a DockerConfig with credentials from the cache if they are
- // found, or from ECR
- func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig {
- parsed, err := parseRepoURL(image)
- if err != nil {
- klog.V(3).Info(err)
- return credentialprovider.DockerConfig{}
- }
- if cfg, exists := p.getFromCache(parsed); exists {
- klog.V(6).Infof("Got ECR credentials from cache for %s", parsed.registry)
- return cfg
- }
- klog.V(3).Info("unable to get ECR credentials from cache, checking ECR API")
- cfg, err := p.getFromECR(parsed)
- if err != nil {
- klog.Errorf("error getting credentials from ECR for %s %v", parsed.registry, err)
- return credentialprovider.DockerConfig{}
- }
- klog.V(3).Infof("Got ECR credentials from ECR API for %s", parsed.registry)
- return cfg
- }
- // getFromCache attempts to get credentials from the cache
- func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.DockerConfig, bool) {
- cfg := credentialprovider.DockerConfig{}
- obj, exists, err := p.cache.GetByKey(parsed.registry)
- if err != nil {
- klog.Errorf("error getting ECR credentials from cache: %v", err)
- return cfg, false
- }
- if !exists {
- return cfg, false
- }
- entry := obj.(*cacheEntry)
- cfg[entry.registry] = entry.credentials
- return cfg, true
- }
- // getFromECR gets credentials from ECR since they are not in the cache
- func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialprovider.DockerConfig, error) {
- cfg := credentialprovider.DockerConfig{}
- getter, err := p.getterFactory.GetTokenGetterForRegion(parsed.region)
- if err != nil {
- return cfg, err
- }
- params := &ecr.GetAuthorizationTokenInput{RegistryIds: []*string{aws.String(parsed.registryID)}}
- output, err := getter.GetAuthorizationToken(params)
- if err != nil {
- return cfg, err
- }
- if output == nil {
- return cfg, errors.New("authorization token is nil")
- }
- if len(output.AuthorizationData) == 0 {
- return cfg, errors.New("authorization data from response is empty")
- }
- data := output.AuthorizationData[0]
- if data.AuthorizationToken == nil {
- return cfg, errors.New("authorization token in response is nil")
- }
- entry, err := makeCacheEntry(data, parsed.registry)
- if err != nil {
- return cfg, err
- }
- if err := p.cache.Add(entry); err != nil {
- return cfg, err
- }
- cfg[entry.registry] = entry.credentials
- return cfg, nil
- }
- type parsedURL struct {
- registryID string
- region string
- registry string
- }
- // parseRepoURL parses and splits the registry URL into the registry ID,
- // region, and registry.
- // <registryID>.dkr.ecr(-fips).<region>.amazonaws.com(.cn)
- func parseRepoURL(image string) (*parsedURL, error) {
- parsed, err := url.Parse("https://" + image)
- if err != nil {
- return nil, fmt.Errorf("error parsing image %s %v", image, err)
- }
- splitURL := ecrPattern.FindStringSubmatch(parsed.Hostname())
- if len(splitURL) == 0 {
- return nil, fmt.Errorf("%s is not a valid ECR repository URL", parsed.Hostname())
- }
- return &parsedURL{
- registryID: splitURL[1],
- region: splitURL[3],
- registry: parsed.Hostname(),
- }, nil
- }
- // tokenGetter is for testing purposes
- type tokenGetter interface {
- GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
- }
- // tokenGetterFactory is for testing purposes
- type tokenGetterFactory interface {
- GetTokenGetterForRegion(string) (tokenGetter, error)
- }
- // ecrTokenGetterFactory stores a token getter per region
- type ecrTokenGetterFactory struct {
- cache map[string]tokenGetter
- mutex sync.Mutex
- }
- // awsHandlerLogger is a handler that logs all AWS SDK requests
- // Copied from pkg/cloudprovider/providers/aws/log_handler.go
- func awsHandlerLogger(req *request.Request) {
- service := req.ClientInfo.ServiceName
- region := req.Config.Region
- name := "?"
- if req.Operation != nil {
- name = req.Operation.Name
- }
- klog.V(3).Infof("AWS request: %s:%s in %s", service, name, *region)
- }
- func newECRTokenGetter(region string) (tokenGetter, error) {
- sess, err := session.NewSessionWithOptions(session.Options{
- Config: aws.Config{Region: aws.String(region)},
- SharedConfigState: session.SharedConfigEnable,
- })
- if err != nil {
- return nil, err
- }
- getter := &ecrTokenGetter{svc: ecr.New(sess)}
- getter.svc.Handlers.Build.PushFrontNamed(request.NamedHandler{
- Name: "k8s/user-agent",
- Fn: request.MakeAddToUserAgentHandler("kubernetes", version.Get().String()),
- })
- getter.svc.Handlers.Sign.PushFrontNamed(request.NamedHandler{
- Name: "k8s/logger",
- Fn: awsHandlerLogger,
- })
- return getter, nil
- }
- // GetTokenGetterForRegion gets the token getter for the requested region. If it
- // doesn't exist, it creates a new ECR token getter
- func (f *ecrTokenGetterFactory) GetTokenGetterForRegion(region string) (tokenGetter, error) {
- f.mutex.Lock()
- defer f.mutex.Unlock()
- if getter, ok := f.cache[region]; ok {
- return getter, nil
- }
- getter, err := newECRTokenGetter(region)
- if err != nil {
- return nil, fmt.Errorf("unable to create token getter for region %v %v", region, err)
- }
- f.cache[region] = getter
- return getter, nil
- }
- // The canonical implementation
- type ecrTokenGetter struct {
- svc *ecr.ECR
- }
- // GetAuthorizationToken gets the ECR authorization token using the ECR API
- func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
- return p.svc.GetAuthorizationToken(input)
- }
- type cacheEntry struct {
- expiresAt time.Time
- credentials credentialprovider.DockerConfigEntry
- registry string
- }
- // makeCacheEntry decodes the ECR authorization entry and re-packages it into a
- // cacheEntry.
- func makeCacheEntry(data *ecr.AuthorizationData, registry string) (*cacheEntry, error) {
- decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken))
- if err != nil {
- return nil, fmt.Errorf("error decoding ECR authorization token: %v", err)
- }
- parts := strings.SplitN(string(decodedToken), ":", 2)
- if len(parts) < 2 {
- return nil, errors.New("error getting username and password from authorization token")
- }
- creds := credentialprovider.DockerConfigEntry{
- Username: parts[0],
- Password: parts[1],
- Email: "not@val.id", // ECR doesn't care and Docker is about to obsolete it
- }
- if data.ExpiresAt == nil {
- return nil, errors.New("authorization data expiresAt is nil")
- }
- return &cacheEntry{
- expiresAt: data.ExpiresAt.Add(-1 * wait.Jitter(30*time.Minute, 0.2)),
- credentials: creds,
- registry: registry,
- }, nil
- }
- // ecrExpirationPolicy implements ExpirationPolicy from client-go.
- type ecrExpirationPolicy struct{}
- // stringKeyFunc returns the cache key as a string
- func stringKeyFunc(obj interface{}) (string, error) {
- key := obj.(*cacheEntry).registry
- return key, nil
- }
- // IsExpired checks if the ECR credentials are expired.
- func (p *ecrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
- return time.Now().After(entry.Obj.(*cacheEntry).expiresAt)
- }
|