Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automated cherry pick of #667: Handle private ECR image references containing public.ecr.aws #682

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 94 additions & 23 deletions cmd/ecr-credential-provider/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ import (
"k8s.io/kubelet/pkg/apis/credentialprovider/v1"
)

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)$`)
const ecrPublicRegion string = "us-east-1"
const ecrPublicHost string = "public.ecr.aws"

var ecrPrivateHostPattern = regexp.MustCompile(`^(\d{12})\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.(amazonaws\.com(\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov)$`)

// ECR abstracts the calls we make to aws-sdk for testing purposes
type ECR interface {
Expand All @@ -59,36 +62,103 @@ func defaultECRProvider(region string, registryID string) (*ecr.ECR, error) {
return ecr.New(sess), nil
}

func (e *ecrPlugin) GetCredentials(ctx context.Context, image string, args []string) (*v1.CredentialProviderResponse, error) {
registryID, region, registry, err := parseRepoURL(image)
func publicECRProvider() (*ecrpublic.ECRPublic, error) {
// ECR public registries are only in one region and only accessible from regions
// in the "aws" partition.
sess, err := session.NewSessionWithOptions(session.Options{
Config: aws.Config{Region: aws.String(ecrPublicRegion)},
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return nil, err
}

return ecrpublic.New(sess), nil
}

type credsData struct {
authToken *string
expiresAt *time.Time
}

func (e *ecrPlugin) getPublicCredsData() (*credsData, error) {
klog.Infof("Getting creds for public registry")
var err error

if e.ecrPublic == nil {
e.ecrPublic, err = publicECRProvider()
}
if err != nil {
return nil, err
}

output, err := e.ecrPublic.GetAuthorizationToken(&ecrpublic.GetAuthorizationTokenInput{})
if err != nil {
return nil, err
}

if output == nil {
return nil, errors.New("response output from ECR was nil")
}

if output.AuthorizationData == nil {
return nil, errors.New("authorization data was empty")
}

return &credsData{
authToken: output.AuthorizationData.AuthorizationToken,
expiresAt: output.AuthorizationData.ExpiresAt,
}, nil
}

func (e *ecrPlugin) getPrivateCredsData(imageHost string, image string) (*credsData, error) {
klog.Infof("Getting creds for private image %s", image)
region, err := parseRegionFromECRPrivateHost(imageHost)
if err != nil {
return nil, err
}
if e.ecr == nil {
e.ecr, err = defaultECRProvider(region, registryID)
if err != nil {
return nil, err
}
}

output, err := e.ecr.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{
RegistryIds: []*string{aws.String(registryID)},
})
output, err := e.ecr.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
if err != nil {
return nil, err
}

if output == nil {
return nil, errors.New("response output from ECR was nil")
}

if len(output.AuthorizationData) == 0 {
return nil, errors.New("authorization data was empty")
}
return &credsData{
authToken: output.AuthorizationData[0].AuthorizationToken,
expiresAt: output.AuthorizationData[0].ExpiresAt,
}, nil
}

func (e *ecrPlugin) GetCredentials(ctx context.Context, image string, args []string) (*v1.CredentialProviderResponse, error) {
var creds *credsData
var err error

imageHost, err := parseHostFromImageReference(image)
if err != nil {
return nil, err
}

if imageHost == ecrPublicHost {
creds, err = e.getPublicCredsData()
} else {
creds, err = e.getPrivateCredsData(imageHost, image)
}

data := output.AuthorizationData[0]
if data.AuthorizationToken == nil {
if err != nil {
return nil, err
}

if creds.authToken == nil {
return nil, errors.New("authorization token in response was nil")
}

Expand All @@ -108,7 +178,7 @@ func (e *ecrPlugin) GetCredentials(ctx context.Context, image string, args []str
CacheKeyType: v1.RegistryPluginCacheKeyType,
CacheDuration: cacheDuration,
Auth: map[string]v1.AuthConfig{
registry: {
imageHost: {
Username: parts[0],
Password: parts[1],
},
Expand All @@ -135,24 +205,25 @@ func getCacheDuration(expiresAt *time.Time) *metav1.Duration {
return cacheDuration
}

// parseRepoURL parses and splits the registry URL
// returns (registryID, region, registry).
// <registryID>.dkr.ecr(-fips).<region>.amazonaws.com(.cn)
func parseRepoURL(image string) (string, string, string, error) {
if !strings.Contains(image, "https://") {
// parseHostFromImageReference parses the hostname from an image reference
func parseHostFromImageReference(image string) (string, error) {
// a URL needs a scheme to be parsed correctly
if !strings.Contains(image, "://") {
image = "https://" + image
}
parsed, err := url.Parse(image)
if err != nil {
return "", "", "", fmt.Errorf("error parsing image %s: %v", image, err)
return "", fmt.Errorf("error parsing image reference %s: %v", image, err)
}
return parsed.Hostname(), nil
}

splitURL := ecrPattern.FindStringSubmatch(parsed.Hostname())
if len(splitURL) < 4 {
return "", "", "", fmt.Errorf("%s is not a valid ECR repository URL", parsed.Hostname())
func parseRegionFromECRPrivateHost(host string) (string, error) {
splitHost := ecrPrivateHostPattern.FindStringSubmatch(host)
if len(splitHost) != 6 {
return "", fmt.Errorf("invalid private ECR host: %s", host)
}

return splitURL[1], splitURL[3], parsed.Hostname(), nil
return splitHost[3], nil
}

func main() {
Expand Down
Loading