keel/extension/credentialshelper/aws/aws.go

179 lines
4.6 KiB
Go

package aws
import (
"encoding/base64"
"fmt"
"net/url"
"regexp"
"strings"
"time"
// "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/keel-hq/keel/extension/credentialshelper"
"github.com/keel-hq/keel/types"
log "github.com/sirupsen/logrus"
)
// AWSCredentialsExpiry specifies how long can we keep cached AWS credentials.
// This is required to reduce chance of hiting rate limits,
// more info here: https://docs.aws.amazon.com/AmazonECR/latest/userguide/service_limits.html
const AWSCredentialsExpiry = 2 * time.Hour
var registryRegxp *regexp.Regexp
func init() {
credentialshelper.RegisterCredentialsHelper("aws", New())
registryRegxp = regexp.MustCompile(`(?P<registryID>\d+)\.dkr\.ecr\.(?P<region>\S+)\.amazonaws\.com`)
}
// CredentialsHelper provides authorization to ECR.
// Authentication details: https://docs.aws.amazon.com/sdk-for-go/api/aws/session/
// # Access Key ID
// AWS_ACCESS_KEY_ID=AKID
// AWS_ACCESS_KEY=AKID # only read if AWS_ACCESS_KEY_ID is not set.
// more on auth: https://stackoverflow.com/questions/41544554/how-to-run-aws-sdk-with-credentials-from-variables
type CredentialsHelper struct {
enabled bool
cache *Cache
}
// New creates a new instance of aws credentials helper
func New() *CredentialsHelper {
ch := &CredentialsHelper{
enabled: true,
cache: NewCache(AWSCredentialsExpiry),
}
return ch
}
// IsEnabled returns a bool whether this credentials helper is initialised or not
func (h *CredentialsHelper) IsEnabled() bool {
return h.enabled
}
// GetCredentials - finds credentials
func (h *CredentialsHelper) GetCredentials(image *types.TrackedImage) (*types.Credentials, error) {
if !h.enabled {
return nil, fmt.Errorf("not initialised")
}
registry := image.Image.Registry()
_, region, err := parseRegistry(registry)
if err != nil {
return nil, err
}
// set region
sess := newAwsSession(region)
svc := ecr.New(sess)
cached, err := h.cache.Get(registry)
if err == nil {
return cached, nil
}
input := &ecr.GetAuthorizationTokenInput{}
result, err := svc.GetAuthorizationToken(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ecr.ErrCodeServerException:
fmt.Println(ecr.ErrCodeServerException, aerr.Error())
case ecr.ErrCodeInvalidParameterException:
fmt.Println(ecr.ErrCodeInvalidParameterException, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
log.WithFields(log.Fields{
"error": err,
}).Error("credentialshelper.aws: failed to get authorization token")
}
return nil, err
}
for _, ad := range result.AuthorizationData {
u, err := url.Parse(*ad.ProxyEndpoint)
if err != nil {
log.WithError(err).Errorf("credentialshelper.aws: failed to parse registry endpoint: %s", *ad.ProxyEndpoint)
continue
}
log.WithFields(log.Fields{
"current_registry": u.Host,
"token": *ad.AuthorizationToken,
"registry": registry,
}).Debug("checking registry")
if u.Host == registry {
username, password, err := decodeBase64Secret(*ad.AuthorizationToken)
if err != nil {
return nil, fmt.Errorf("failed to decode authentication token: %s, error: %s", *ad.AuthorizationToken, err)
}
creds := &types.Credentials{
Username: username,
Password: password,
}
h.cache.Put(registry, creds)
return creds, nil
}
}
return nil, fmt.Errorf("not found")
}
func newAwsSession(region string) *session.Session {
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String(region),
}))
return sess
}
func decodeBase64Secret(authSecret string) (username, password string, err error) {
decoded, err := base64.StdEncoding.DecodeString(authSecret)
if err != nil {
return
}
parts := strings.Split(string(decoded), ":")
if len(parts) != 2 {
return "", "", fmt.Errorf("unexpected auth secret format")
}
return parts[0], parts[1], nil
}
func parseRegistry(registry string) (registryID string, region string, err error) {
if !registryRegxp.MatchString(registry) {
err = credentialshelper.ErrUnsupportedRegistry
return
}
// parse registry with named regex, then put into map by name
matches := registryRegxp.FindStringSubmatch(registry)
registryParsed := make(map[string]string)
for i, name := range registryRegxp.SubexpNames() {
if i != 0 && name != "" {
registryParsed[name] = matches[i]
}
}
return registryParsed["registryID"], registryParsed["region"], nil
}