Skip to content

Commit

Permalink
Merge pull request #203 from keel-hq/feature/ecr_token_cache
Browse files Browse the repository at this point in the history
cache for credentials
rusenask authored Apr 29, 2018

Verified

This commit was signed with the committer’s verified signature.
DavyLandman Davy Landman
2 parents 08468c2 + 3d97b2e commit ab8420c
Showing 4 changed files with 184 additions and 7 deletions.
36 changes: 29 additions & 7 deletions extension/credentialshelper/aws/aws.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import (
"net/url"
"os"
"strings"
"time"

// "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws"
@@ -19,6 +20,11 @@ import (
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

func init() {
credentialshelper.RegisterCredentialsHelper("aws", New())
}
@@ -32,6 +38,7 @@ func init() {
type CredentialsHelper struct {
enabled bool
region string
cache *Cache
}

// New creates a new instance of aws credentials helper
@@ -43,16 +50,18 @@ func New() *CredentialsHelper {
Region: aws.String(region),
})

_, err := svc.ListImages(&ecr.ListImagesInput{})
if err == nil {
_, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
if err != nil {
if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
log.WithError(err).Error("extension.credentialshelper.aws: environment variables are set but initiliasation failed")
}
} else {
ch.enabled = true
log.Infof("extension.credentialshelper.aws: enabled")
ch.region = region
ch.cache = NewCache(AWSCredentialsExpiry)
}

// if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" && os.Getenv("AWS_REGION") != "" {
// }

return ch
}

@@ -64,12 +73,21 @@ func (h *CredentialsHelper) IsEnabled() bool {
// 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()

if !strings.Contains(registry, "amazonaws.com") {
return nil, credentialshelper.ErrUnsupportedRegistry
}

cached, err := h.cache.Get(registry)
if err == nil {
return cached, nil
}

svc := ecr.New(session.New(), &aws.Config{
Region: aws.String(h.region),
})
@@ -116,10 +134,14 @@ func (h *CredentialsHelper) GetCredentials(image *types.TrackedImage) (*types.Cr
return nil, fmt.Errorf("failed to decode authentication token: %s, error: %s", *ad.AuthorizationToken, err)
}

return &types.Credentials{
creds := &types.Credentials{
Username: username,
Password: password,
}, nil
}

h.cache.Put(registry, creds)

return creds, nil
}
}

17 changes: 17 additions & 0 deletions extension/credentialshelper/aws/aws_test.go
Original file line number Diff line number Diff line change
@@ -45,3 +45,20 @@ func TestAWS(t *testing.T) {
t.Errorf("unexpected digest: %s", currentDigest)
}
}

func TestCredentialsCaching(t *testing.T) {

if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
t.Skip()
}
ch := New()
imgRef, _ := image.Parse("528670773427.dkr.ecr.us-east-2.amazonaws.com/webhook-demo:master")
for i := 0; i < 200; i++ {
_, err := ch.GetCredentials(&types.TrackedImage{
Image: imgRef,
})
if err != nil {
t.Fatalf("cred helper got error: %s", err)
}
}
}
79 changes: 79 additions & 0 deletions extension/credentialshelper/aws/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package aws

import (
"fmt"
"sync"
"time"

"github.com/keel-hq/keel/types"
)

type item struct {
credentials *types.Credentials
created time.Time
}

// Cache - internal cache for aws
type Cache struct {
creds map[string]*item
tick time.Duration
ttl time.Duration
mu *sync.RWMutex
}

// NewCache - new credentials cache
func NewCache(ttl time.Duration) *Cache {
c := &Cache{
creds: make(map[string]*item),
ttl: ttl,
tick: 30 * time.Second,
mu: &sync.RWMutex{},
}
go c.expiryService()
return c
}

func (c *Cache) expiryService() {
ticker := time.NewTicker(c.tick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.expire()
}
}
}

func (c *Cache) expire() {
c.mu.Lock()
defer c.mu.Unlock()
t := time.Now()
for k, v := range c.creds {
if t.Sub(v.created) > c.ttl {
delete(c.creds, k)
}
}
}

// Put - saves new creds
func (c *Cache) Put(registry string, creds *types.Credentials) {
c.mu.Lock()
defer c.mu.Unlock()
c.creds[registry] = &item{credentials: creds, created: time.Now()}
}

// Get - retrieves creds
func (c *Cache) Get(registry string) (*types.Credentials, error) {
c.mu.RLock()
defer c.mu.RUnlock()

item, ok := c.creds[registry]
if !ok {
return nil, fmt.Errorf("not found")
}

cr := new(types.Credentials)
*cr = *item.credentials

return cr, nil
}
59 changes: 59 additions & 0 deletions extension/credentialshelper/aws/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package aws

import (
"sync"
"time"

"github.com/keel-hq/keel/types"

"testing"
)

func TestPutCreds(t *testing.T) {
c := NewCache(time.Second * 5)

creds := &types.Credentials{
Username: "user-1",
Password: "pass-1",
}

c.Put("reg1", creds)

stored, err := c.Get("reg1")
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if stored.Username != "user-1" {
t.Errorf("username mismatch: %s", stored.Username)
}
if stored.Password != "pass-1" {
t.Errorf("password mismatch: %s", stored.Password)
}
}

func TestExpiry(t *testing.T) {
c := &Cache{
creds: make(map[string]*item),
mu: &sync.RWMutex{},
ttl: time.Millisecond * 500,
tick: time.Millisecond * 100,
}

go c.expiryService()

creds := &types.Credentials{
Username: "user-1",
Password: "pass-1",
}

c.Put("reg1", creds)

time.Sleep(1100 * time.Millisecond)

_, err := c.Get("reg1")
if err == nil {
t.Fatalf("expected to get an error about missing record")
}

}

0 comments on commit ab8420c

Please sign in to comment.