| /* |
| 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" |
| "fmt" |
| "strings" |
| "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/klog" |
| |
| "k8s.io/kubernetes/pkg/credentialprovider" |
| "k8s.io/kubernetes/pkg/version" |
| ) |
| |
| const awsChinaRegionPrefix = "cn-" |
| const awsStandardDNSSuffix = "amazonaws.com" |
| const awsChinaDNSSuffix = "amazonaws.com.cn" |
| const registryURLTemplate = "*.dkr.ecr.%s.%s" |
| |
| // 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) |
| } |
| |
| // An interface for testing purposes. |
| type tokenGetter interface { |
| GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) |
| } |
| |
| // The canonical implementation |
| type ecrTokenGetter struct { |
| svc *ecr.ECR |
| } |
| |
| func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) { |
| return p.svc.GetAuthorizationToken(input) |
| } |
| |
| // lazyEcrProvider is a DockerConfigProvider that creates on demand an |
| // ecrProvider for a given region and then proxies requests to it. |
| type lazyEcrProvider struct { |
| region string |
| regionURL string |
| actualProvider *credentialprovider.CachingDockerConfigProvider |
| } |
| |
| var _ credentialprovider.DockerConfigProvider = &lazyEcrProvider{} |
| |
| // ecrProvider is a DockerConfigProvider that gets and refreshes 12-hour tokens |
| // from AWS to access ECR. |
| type ecrProvider struct { |
| region string |
| regionURL string |
| getter tokenGetter |
| } |
| |
| var _ credentialprovider.DockerConfigProvider = &ecrProvider{} |
| |
| // registryURL has different suffix in AWS China region |
| func registryURL(region string) string { |
| dnsSuffix := awsStandardDNSSuffix |
| // deal with aws none standard regions |
| if strings.HasPrefix(region, awsChinaRegionPrefix) { |
| dnsSuffix = awsChinaDNSSuffix |
| } |
| return fmt.Sprintf(registryURLTemplate, region, dnsSuffix) |
| } |
| |
| // RegisterCredentialsProvider registers a credential provider for the specified region. |
| // It creates a lazy provider for each AWS region, in order to support |
| // cross-region ECR access. They have to be lazy because it's unlikely, but not |
| // impossible, that we'll use more than one. |
| // This should be called only if using the AWS cloud provider. |
| // This way, we avoid timeouts waiting for a non-existent provider. |
| func RegisterCredentialsProvider(region string) { |
| klog.V(4).Infof("registering credentials provider for AWS region %q", region) |
| |
| credentialprovider.RegisterCredentialProvider("aws-ecr-"+region, |
| &lazyEcrProvider{ |
| region: region, |
| regionURL: registryURL(region), |
| }) |
| } |
| |
| // Enabled implements DockerConfigProvider.Enabled for the lazy provider. |
| // Since we perform no checks/work of our own and actualProvider is only created |
| // later at image pulling time (if ever), always return true. |
| func (p *lazyEcrProvider) Enabled() bool { |
| return true |
| } |
| |
| // LazyProvide implements DockerConfigProvider.LazyProvide. It will be called |
| // by the client when attempting to pull an image and it will create the actual |
| // provider only when we actually need it the first time. |
| func (p *lazyEcrProvider) LazyProvide() *credentialprovider.DockerConfigEntry { |
| if p.actualProvider == nil { |
| klog.V(2).Infof("Creating ecrProvider for %s", p.region) |
| p.actualProvider = &credentialprovider.CachingDockerConfigProvider{ |
| Provider: newEcrProvider(p.region, nil), |
| // Refresh credentials a little earlier than expiration time |
| Lifetime: 11*time.Hour + 55*time.Minute, |
| } |
| if !p.actualProvider.Enabled() { |
| return nil |
| } |
| } |
| entry := p.actualProvider.Provide()[p.regionURL] |
| return &entry |
| } |
| |
| // Provide implements DockerConfigProvider.Provide, creating dummy credentials. |
| // Client code will call Provider.LazyProvide() at image pulling time. |
| func (p *lazyEcrProvider) Provide() credentialprovider.DockerConfig { |
| entry := credentialprovider.DockerConfigEntry{ |
| Provider: p, |
| } |
| cfg := credentialprovider.DockerConfig{} |
| cfg[p.regionURL] = entry |
| return cfg |
| } |
| |
| func newEcrProvider(region string, getter tokenGetter) *ecrProvider { |
| return &ecrProvider{ |
| region: region, |
| regionURL: registryURL(region), |
| getter: getter, |
| } |
| } |
| |
| // Enabled implements DockerConfigProvider.Enabled for the AWS token-based implementation. |
| // For now, it gets activated only if AWS was chosen as the cloud provider. |
| // TODO: figure how to enable it manually for deployments that are not on AWS but still |
| // use ECR somehow? |
| func (p *ecrProvider) Enabled() bool { |
| if p.region == "" { |
| klog.Errorf("Called ecrProvider.Enabled() with no region set") |
| return false |
| } |
| |
| getter := &ecrTokenGetter{svc: ecr.New(session.New(&aws.Config{ |
| Credentials: nil, |
| Region: &p.region, |
| }))} |
| 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, |
| }) |
| p.getter = getter |
| |
| return true |
| } |
| |
| // LazyProvide implements DockerConfigProvider.LazyProvide. Should never be called. |
| func (p *ecrProvider) LazyProvide() *credentialprovider.DockerConfigEntry { |
| return nil |
| } |
| |
| // Provide implements DockerConfigProvider.Provide, refreshing ECR tokens on demand |
| func (p *ecrProvider) Provide() credentialprovider.DockerConfig { |
| cfg := credentialprovider.DockerConfig{} |
| |
| // TODO: fill in RegistryIds? |
| params := &ecr.GetAuthorizationTokenInput{} |
| output, err := p.getter.GetAuthorizationToken(params) |
| if err != nil { |
| klog.Errorf("while requesting ECR authorization token %v", err) |
| return cfg |
| } |
| if output == nil { |
| klog.Errorf("Got back no ECR token") |
| return cfg |
| } |
| |
| for _, data := range output.AuthorizationData { |
| if data.ProxyEndpoint != nil && |
| data.AuthorizationToken != nil { |
| decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken)) |
| if err != nil { |
| klog.Errorf("while decoding token for endpoint %v %v", data.ProxyEndpoint, err) |
| return cfg |
| } |
| parts := strings.SplitN(string(decodedToken), ":", 2) |
| user := parts[0] |
| password := parts[1] |
| entry := credentialprovider.DockerConfigEntry{ |
| Username: user, |
| Password: password, |
| // ECR doesn't care and Docker is about to obsolete it |
| Email: "not@val.id", |
| } |
| |
| klog.V(3).Infof("Adding credentials for user %s in %s", user, p.region) |
| // Add our config entry for this region's registry URLs |
| cfg[p.regionURL] = entry |
| |
| } |
| } |
| return cfg |
| } |