-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for aws ecr tokens (#2650)
Signed-off-by: K Tamil Vanan <[email protected]>
- Loading branch information
Showing
10 changed files
with
470 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"distSpecVersion": "1.1.0", | ||
"storage": { | ||
"rootDirectory": "/tmp/zot", | ||
"dedupe": false, | ||
"storageDriver": { | ||
"name": "s3", | ||
"region": "REGION_NAME", | ||
"bucket": "BUGKET_NAME", | ||
"rootdirectory": "/ROOTDIR", | ||
"secure": true, | ||
"skipverify": false | ||
} | ||
}, | ||
"http": { | ||
"address": "0.0.0.0", | ||
"port": "8080" | ||
}, | ||
"log": { | ||
"level": "debug" | ||
}, | ||
"extensions": { | ||
"sync": { | ||
"credentialsFile": "", | ||
"DownloadDir": "/tmp/zot", | ||
"registries": [ | ||
{ | ||
"urls": [ | ||
"https://ACCOUNTID.dkr.ecr.REGION.amazonaws.com" | ||
], | ||
"onDemand": true, | ||
"maxRetries": 5, | ||
"retryDelay": "2m", | ||
"credentialHelper": "ecr" | ||
} | ||
] | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
//go:build sync | ||
// +build sync | ||
|
||
package sync | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go-v2/config" | ||
"github.com/aws/aws-sdk-go-v2/service/ecr" | ||
|
||
syncconf "zotregistry.dev/zot/pkg/extensions/config/sync" | ||
"zotregistry.dev/zot/pkg/log" | ||
) | ||
|
||
// ECR tokens are valid for 12 hours. The ExpiryWindow variable is set to 1 hour, | ||
// meaning if the remaining validity of the token is less than 1 hour, it will be considered expired. | ||
const ( | ||
ExpiryWindow int = 1 | ||
ECRURLSplitPartsCount int = 6 | ||
UsernameTokenParts int = 2 | ||
) | ||
|
||
var ( | ||
ErrInvalidURLFormat = errors.New("invalid ECR URL is received") | ||
ErrInvalidTokenFormat = errors.New("invalid token format received from ECR") | ||
ErrUnableToLoadAWSConfig = errors.New("unable to load AWS config for region") | ||
ErrUnableToGetECRAuthToken = errors.New("unable to get ECR authorization token for account") | ||
ErrUnableToDecodeECRToken = errors.New("unable to decode ECR token") | ||
ErrFailedToGetECRCredentials = errors.New("failed to get ECR credentials") | ||
) | ||
|
||
type ECRCredential struct { | ||
username string | ||
password string | ||
expiry time.Time | ||
account string | ||
region string | ||
} | ||
|
||
type ECRCredentialsHelper struct { | ||
credentials map[string]ECRCredential | ||
log log.Logger | ||
} | ||
|
||
func NewECRCredentialHelper(log log.Logger) CredentialHelper { | ||
return &ECRCredentialsHelper{ | ||
credentials: make(map[string]ECRCredential), | ||
log: log, | ||
} | ||
} | ||
|
||
// extractAccountAndRegion extracts the account ID and region from the given ECR URL. | ||
// Example URL format: account.dkr.ecr.region.amazonaws.com. | ||
func extractAccountAndRegion(url string) (string, string, error) { | ||
parts := strings.Split(url, ".") | ||
if len(parts) < ECRURLSplitPartsCount { | ||
return "", "", fmt.Errorf("%w: %s", ErrInvalidURLFormat, url) | ||
} | ||
|
||
accountID := parts[0] // First part is the account ID | ||
region := parts[3] // Fourth part is the region | ||
|
||
return accountID, region, nil | ||
} | ||
|
||
func getECRCredentials(remoteAddress string) (ECRCredential, error) { | ||
// Extract account ID and region from the URL. | ||
accountID, region, err := extractAccountAndRegion(remoteAddress) | ||
if err != nil { | ||
return ECRCredential{}, fmt.Errorf("%w %s: %w", ErrInvalidTokenFormat, remoteAddress, err) | ||
} | ||
|
||
// Load the AWS config for the specific region. | ||
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) | ||
if err != nil { | ||
return ECRCredential{}, fmt.Errorf("%w %s: %w", ErrUnableToLoadAWSConfig, region, err) | ||
} | ||
|
||
// Create an ECR client | ||
ecrClient := ecr.NewFromConfig(cfg) | ||
|
||
// Fetch the ECR authorization token. | ||
ecrAuth, err := ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{ | ||
RegistryIds: []string{accountID}, // Filter by the account ID. | ||
}) | ||
if err != nil { | ||
return ECRCredential{}, fmt.Errorf("%w %s: %w", ErrUnableToGetECRAuthToken, accountID, err) | ||
} | ||
|
||
// Decode the base64-encoded ECR token. | ||
authToken := *ecrAuth.AuthorizationData[0].AuthorizationToken | ||
decodedToken, err := base64.StdEncoding.DecodeString(authToken) | ||
|
||
if err != nil { | ||
return ECRCredential{}, fmt.Errorf("%w: %w", ErrUnableToDecodeECRToken, err) | ||
} | ||
|
||
// Split the decoded token into username and password (username is "AWS"). | ||
tokenParts := strings.Split(string(decodedToken), ":") | ||
if len(tokenParts) != UsernameTokenParts { | ||
return ECRCredential{}, fmt.Errorf("%w", ErrInvalidTokenFormat) | ||
} | ||
|
||
expiry := *ecrAuth.AuthorizationData[0].ExpiresAt | ||
username := tokenParts[0] | ||
password := tokenParts[1] | ||
|
||
return ECRCredential{username: username, password: password, expiry: expiry, account: accountID, region: region}, nil | ||
} | ||
|
||
// GetECRCredentials retrieves the ECR credentials (username and password) from AWS ECR. | ||
func (credHelper *ECRCredentialsHelper) GetCredentials(urls []string) (syncconf.CredentialsFile, error) { | ||
ecrCredentials := make(syncconf.CredentialsFile) | ||
|
||
for _, url := range urls { | ||
remoteAddress := StripRegistryTransport(url) | ||
ecrCred, err := getECRCredentials(remoteAddress) | ||
|
||
if err != nil { | ||
return syncconf.CredentialsFile{}, fmt.Errorf("%w %s: %w", ErrFailedToGetECRCredentials, url, err) | ||
} | ||
// Store the credentials in the map using the base URL as the key. | ||
ecrCredentials[remoteAddress] = syncconf.Credentials{ | ||
Username: ecrCred.username, | ||
Password: ecrCred.password, | ||
} | ||
credHelper.credentials[remoteAddress] = ecrCred | ||
} | ||
|
||
return ecrCredentials, nil | ||
} | ||
|
||
func (credHelper *ECRCredentialsHelper) IsCredentialsValid(remoteAddress string) bool { | ||
expiry := credHelper.credentials[remoteAddress].expiry | ||
expiryDuration := time.Duration(ExpiryWindow) * time.Hour | ||
|
||
if time.Until(expiry) <= expiryDuration { | ||
credHelper.log.Info().Str("url", remoteAddress).Msg("The credentials are close to expiring") | ||
|
||
return false | ||
} | ||
credHelper.log.Info().Str("url", remoteAddress).Msg("The credentials are valid") | ||
|
||
return true | ||
} | ||
|
||
func (credHelper *ECRCredentialsHelper) RefreshCredentials(remoteAddress string) (syncconf.Credentials, error) { | ||
credHelper.log.Info().Str("url", remoteAddress).Msg("Refreshing the ECR credentials") | ||
ecrCred, err := getECRCredentials(remoteAddress) | ||
|
||
if err != nil { | ||
return syncconf.Credentials{}, fmt.Errorf("%w %s: %w", ErrFailedToGetECRCredentials, remoteAddress, err) | ||
} | ||
|
||
return syncconf.Credentials{Username: ecrCred.username, Password: ecrCred.password}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.