kaniko/vendor/github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api/client.go

302 lines
10 KiB
Go

// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 api
import (
"context"
"encoding/base64"
"fmt"
"net/url"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
"github.com/sirupsen/logrus"
"github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cache"
)
const (
proxyEndpointScheme = "https://"
programName = "docker-credential-ecr-login"
ecrPublicName = "public.ecr.aws"
ecrPublicEndpoint = proxyEndpointScheme + ecrPublicName
)
var ecrPattern = regexp.MustCompile(`^(\d{12})\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.(amazonaws\.com(\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov)$`)
type Service string
const (
ServiceECR Service = "ecr"
ServiceECRPublic Service = "ecr-public"
)
// Registry in ECR
type Registry struct {
Service Service
ID string
FIPS bool
Region string
}
// ExtractRegistry returns the ECR registry behind a given service endpoint
func ExtractRegistry(input string) (*Registry, error) {
if strings.HasPrefix(input, proxyEndpointScheme) {
input = strings.TrimPrefix(input, proxyEndpointScheme)
}
serverURL, err := url.Parse(proxyEndpointScheme + input)
if err != nil {
return nil, err
}
if serverURL.Hostname() == ecrPublicName {
return &Registry{
Service: ServiceECRPublic,
}, nil
}
matches := ecrPattern.FindStringSubmatch(serverURL.Hostname())
if len(matches) == 0 {
return nil, fmt.Errorf(programName + " can only be used with Amazon Elastic Container Registry.")
} else if len(matches) < 3 {
return nil, fmt.Errorf("%q is not a valid repository URI for Amazon Elastic Container Registry.", input)
}
return &Registry{
Service: ServiceECR,
ID: matches[1],
FIPS: matches[2] == "-fips",
Region: matches[3],
}, nil
}
// Client used for calling ECR service
type Client interface {
GetCredentials(serverURL string) (*Auth, error)
GetCredentialsByRegistryID(registryID string) (*Auth, error)
ListCredentials() ([]*Auth, error)
}
// Auth credentials returned by ECR service to allow docker login
type Auth struct {
ProxyEndpoint string
Username string
Password string
}
type defaultClient struct {
ecrClient ECRAPI
ecrPublicClient ECRPublicAPI
credentialCache cache.CredentialsCache
}
type ECRAPI interface {
GetAuthorizationToken(context.Context, *ecr.GetAuthorizationTokenInput, ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)
}
type ECRPublicAPI interface {
GetAuthorizationToken(context.Context, *ecrpublic.GetAuthorizationTokenInput, ...func(*ecrpublic.Options)) (*ecrpublic.GetAuthorizationTokenOutput, error)
}
// GetCredentials returns username, password, and proxyEndpoint
func (c *defaultClient) GetCredentials(serverURL string) (*Auth, error) {
registry, err := ExtractRegistry(serverURL)
if err != nil {
return nil, err
}
logrus.
WithField("service", registry.Service).
WithField("registry", registry.ID).
WithField("region", registry.Region).
WithField("serverURL", serverURL).
Debug("Retrieving credentials")
switch registry.Service {
case ServiceECR:
return c.GetCredentialsByRegistryID(registry.ID)
case ServiceECRPublic:
return c.GetPublicCredentials()
}
return nil, fmt.Errorf("unknown service %q", registry.Service)
}
// GetCredentialsByRegistryID returns username, password, and proxyEndpoint
func (c *defaultClient) GetCredentialsByRegistryID(registryID string) (*Auth, error) {
cachedEntry := c.credentialCache.Get(registryID)
if cachedEntry != nil {
if cachedEntry.IsValid(time.Now()) {
logrus.WithField("registry", registryID).Debug("Using cached token")
return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint)
}
logrus.
WithField("requestedAt", cachedEntry.RequestedAt).
WithField("expiresAt", cachedEntry.ExpiresAt).
Debug("Cached token is no longer valid")
}
auth, err := c.getAuthorizationToken(registryID)
// if we have a cached token, fall back to avoid failing the request. This may result an expired token
// being returned, but if there is a 500 or timeout from the service side, we'd like to attempt to re-use an
// old token. We invalidate tokens prior to their expiration date to help mitigate this scenario.
if err != nil && cachedEntry != nil {
logrus.WithError(err).Info("Got error fetching authorization token. Falling back to cached token.")
return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint)
}
return auth, err
}
func (c *defaultClient) GetPublicCredentials() (*Auth, error) {
cachedEntry := c.credentialCache.GetPublic()
if cachedEntry != nil {
if cachedEntry.IsValid(time.Now()) {
logrus.WithField("registry", ecrPublicName).Debug("Using cached token")
return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint)
}
logrus.
WithField("requestedAt", cachedEntry.RequestedAt).
WithField("expiresAt", cachedEntry.ExpiresAt).
Debug("Cached token is no longer valid")
}
auth, err := c.getPublicAuthorizationToken()
// if we have a cached token, fall back to avoid failing the request. This may result an expired token
// being returned, but if there is a 500 or timeout from the service side, we'd like to attempt to re-use an
// old token. We invalidate tokens prior to their expiration date to help mitigate this scenario.
if err != nil && cachedEntry != nil {
logrus.WithError(err).Info("Got error fetching authorization token. Falling back to cached token.")
return extractToken(cachedEntry.AuthorizationToken, cachedEntry.ProxyEndpoint)
}
return auth, err
}
func (c *defaultClient) ListCredentials() ([]*Auth, error) {
// prime the cache with default authorization tokens
_, err := c.GetCredentialsByRegistryID("")
if err != nil {
logrus.WithError(err).Debug("couldn't get authorization token for default registry")
}
_, err = c.GetPublicCredentials()
if err != nil {
logrus.WithError(err).Debug("couldn't get authorization token for public registry")
}
auths := make([]*Auth, 0)
for _, authEntry := range c.credentialCache.List() {
auth, err := extractToken(authEntry.AuthorizationToken, authEntry.ProxyEndpoint)
if err != nil {
logrus.WithError(err).Debug("Could not extract token")
} else {
auths = append(auths, auth)
}
}
return auths, nil
}
func (c *defaultClient) getAuthorizationToken(registryID string) (*Auth, error) {
var input *ecr.GetAuthorizationTokenInput
if registryID == "" {
logrus.Debug("Calling ECR.GetAuthorizationToken for default registry")
input = &ecr.GetAuthorizationTokenInput{}
} else {
logrus.WithField("registry", registryID).Debug("Calling ECR.GetAuthorizationToken")
input = &ecr.GetAuthorizationTokenInput{
RegistryIds: []string{registryID},
}
}
output, err := c.ecrClient.GetAuthorizationToken(context.TODO(), input)
if err != nil || output == nil {
if err == nil {
if registryID == "" {
err = fmt.Errorf("missing AuthorizationData in ECR response for default registry")
} else {
err = fmt.Errorf("missing AuthorizationData in ECR response for %s", registryID)
}
}
return nil, fmt.Errorf("ecr: Failed to get authorization token: %w", err)
}
for _, authData := range output.AuthorizationData {
if authData.ProxyEndpoint != nil && authData.AuthorizationToken != nil {
authEntry := cache.AuthEntry{
AuthorizationToken: aws.ToString(authData.AuthorizationToken),
RequestedAt: time.Now(),
ExpiresAt: aws.ToTime(authData.ExpiresAt),
ProxyEndpoint: aws.ToString(authData.ProxyEndpoint),
Service: cache.ServiceECR,
}
registry, err := ExtractRegistry(authEntry.ProxyEndpoint)
if err != nil {
return nil, fmt.Errorf("Invalid ProxyEndpoint returned by ECR: %s", authEntry.ProxyEndpoint)
}
auth, err := extractToken(authEntry.AuthorizationToken, authEntry.ProxyEndpoint)
if err != nil {
return nil, err
}
c.credentialCache.Set(registry.ID, &authEntry)
return auth, nil
}
}
if registryID == "" {
return nil, fmt.Errorf("No AuthorizationToken found for default registry")
}
return nil, fmt.Errorf("No AuthorizationToken found for %s", registryID)
}
func (c *defaultClient) getPublicAuthorizationToken() (*Auth, error) {
var input *ecrpublic.GetAuthorizationTokenInput
output, err := c.ecrPublicClient.GetAuthorizationToken(context.TODO(), input)
if err != nil {
return nil, fmt.Errorf("ecr: failed to get authorization token: %w", err)
}
if output == nil || output.AuthorizationData == nil {
return nil, fmt.Errorf("ecr: missing AuthorizationData in ECR Public response")
}
authData := output.AuthorizationData
token, err := extractToken(aws.ToString(authData.AuthorizationToken), ecrPublicEndpoint)
if err != nil {
return nil, err
}
authEntry := cache.AuthEntry{
AuthorizationToken: aws.ToString(authData.AuthorizationToken),
RequestedAt: time.Now(),
ExpiresAt: aws.ToTime(authData.ExpiresAt),
ProxyEndpoint: ecrPublicEndpoint,
Service: cache.ServiceECRPublic,
}
c.credentialCache.Set(ecrPublicName, &authEntry)
return token, nil
}
func extractToken(token string, proxyEndpoint string) (*Auth, error) {
decodedToken, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
parts := strings.SplitN(string(decodedToken), ":", 2)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid token: expected two parts, got %d", len(parts))
}
return &Auth{
Username: parts[0],
Password: parts[1],
ProxyEndpoint: proxyEndpoint,
}, nil
}