Skip to content

Commit e8acb08

Browse files
committed
Fix memory leak issue by implementing an LRU cache for AWS credentials.
Signed-off-by: Dan Lorenc <[email protected]>
1 parent e58d7f5 commit e8acb08

File tree

5 files changed

+703
-1
lines changed

5 files changed

+703
-1
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ require (
191191
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
192192
github.com/gorilla/mux v1.8.1 // indirect
193193
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
194+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
194195
github.com/hashicorp/vault/api v1.16.0 // indirect
195196
github.com/in-toto/attestation v1.1.1 // indirect
196197
github.com/in-toto/in-toto-golang v0.9.0 // indirect
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//
2+
// Copyright 2024 The Sigstore Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package registryauth
17+
18+
import (
19+
"errors"
20+
"io"
21+
"sync"
22+
"time"
23+
24+
ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
25+
lru "github.com/hashicorp/golang-lru/v2"
26+
)
27+
28+
// Credential represents a username/password pair for a specific registry server
29+
type Credential struct {
30+
ServerURL string
31+
Username string
32+
Password string
33+
}
34+
35+
// ErrCredentialsNotFound is returned when credentials are not found
36+
var ErrCredentialsNotFound = errors.New("credentials not found")
37+
38+
// CredentialHelper defines the interface for credential helpers
39+
type CredentialHelper interface {
40+
Get(string) (string, string, error)
41+
Add(interface{}) error
42+
Delete(string) error
43+
List() (map[string]string, error)
44+
}
45+
46+
// ECRCredentialCache wraps the ECR credential helper with a bounded LRU cache
47+
// to prevent memory leaks in long-running processes
48+
type ECRCredentialCache struct {
49+
// LRU cache for storing credentials with an eviction policy
50+
cache *lru.Cache[string, Credential]
51+
// The underlying ECR credential helper
52+
helper CredentialHelper
53+
// Mutex for concurrent access
54+
mu sync.Mutex
55+
// Time when entries should expire (enforce re-fetching credentials)
56+
ttl time.Duration
57+
// Cache entries have an expiration timestamp
58+
expiry map[string]time.Time
59+
}
60+
61+
// ECRHelperAdapter adapts the ECR helper to our CredentialHelper interface
62+
type ECRHelperAdapter struct {
63+
helper *ecr.ECRHelper
64+
}
65+
66+
// NewECRHelperAdapter creates a new adapter for the ECR helper
67+
func NewECRHelperAdapter(helper *ecr.ECRHelper) *ECRHelperAdapter {
68+
return &ECRHelperAdapter{helper: helper}
69+
}
70+
71+
// Get delegates to the underlying ECR helper
72+
func (a *ECRHelperAdapter) Get(serverURL string) (string, string, error) {
73+
return a.helper.Get(serverURL)
74+
}
75+
76+
// Add delegates to the underlying ECR helper
77+
func (a *ECRHelperAdapter) Add(_ interface{}) error {
78+
return ErrCredentialsNotFound
79+
}
80+
81+
// Delete delegates to the underlying ECR helper
82+
func (a *ECRHelperAdapter) Delete(serverURL string) error {
83+
return ErrCredentialsNotFound
84+
}
85+
86+
// List delegates to the underlying ECR helper
87+
func (a *ECRHelperAdapter) List() (map[string]string, error) {
88+
return a.helper.List()
89+
}
90+
91+
// NewECRCredentialCache creates a new credential cache with bounded memory.
92+
// The cacheSize parameter defines the maximum number of entries.
93+
// The ttl parameter defines how long an entry is valid for.
94+
func NewECRCredentialCache(cacheSize int, ttl time.Duration) (*ECRCredentialCache, error) {
95+
// Create an LRU cache with a fixed size
96+
cache, err := lru.New[string, Credential](cacheSize)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
// Create the ECR helper with discarded logging
102+
ecrHelper := ecr.NewECRHelper(ecr.WithLogger(io.Discard))
103+
104+
// Adapt the ECR helper to our interface
105+
adapter := NewECRHelperAdapter(ecrHelper)
106+
107+
return &ECRCredentialCache{
108+
cache: cache,
109+
helper: adapter,
110+
ttl: ttl,
111+
expiry: make(map[string]time.Time),
112+
}, nil
113+
}
114+
115+
// Get retrieves credentials from the cache if they exist and are valid,
116+
// otherwise it fetches new credentials from ECR
117+
func (c *ECRCredentialCache) Get(serverURL string) (string, string, error) {
118+
c.mu.Lock()
119+
defer c.mu.Unlock()
120+
121+
now := time.Now()
122+
123+
// Check if we have a valid cache entry
124+
if creds, ok := c.cache.Get(serverURL); ok {
125+
// Check if the entry has expired
126+
if expiry, exists := c.expiry[serverURL]; exists && now.Before(expiry) {
127+
// Return the cached credentials
128+
return creds.Username, creds.Password, nil
129+
}
130+
// Entry has expired, remove it from the cache
131+
c.cache.Remove(serverURL)
132+
delete(c.expiry, serverURL)
133+
}
134+
135+
// Fetch fresh credentials from ECR
136+
username, password, err := c.helper.Get(serverURL)
137+
if err != nil {
138+
return "", "", err
139+
}
140+
141+
// Cache the new credentials with an expiry
142+
c.cache.Add(serverURL, Credential{
143+
ServerURL: serverURL,
144+
Username: username,
145+
Password: password,
146+
})
147+
c.expiry[serverURL] = now.Add(c.ttl)
148+
149+
return username, password, nil
150+
}
151+
152+
// GetEnvVars returns the environment variables with AWS_ECR_DISABLE_CACHE set to true
153+
func GetEnvVars() []string {
154+
return []string{"AWS_ECR_DISABLE_CACHE=true"}
155+
}
156+
157+
// DockerCredentialHelper adapts the ECRCredentialCache to implement the
158+
// docker-credential-helpers interface
159+
type DockerCredentialHelper struct {
160+
cache *ECRCredentialCache
161+
}
162+
163+
// NewDockerCredentialHelper creates a new helper that satisfies the docker credentials interface
164+
func NewDockerCredentialHelper(cache *ECRCredentialCache) *DockerCredentialHelper {
165+
return &DockerCredentialHelper{
166+
cache: cache,
167+
}
168+
}
169+
170+
// Add is not supported, as ECR only uses temporary credentials
171+
func (d *DockerCredentialHelper) Add(creds interface{}) error {
172+
return ErrCredentialsNotFound
173+
}
174+
175+
// Delete is not supported, as ECR only uses temporary credentials
176+
func (d *DockerCredentialHelper) Delete(serverURL string) error {
177+
return ErrCredentialsNotFound
178+
}
179+
180+
// Get retrieves credentials for the given server URL
181+
func (d *DockerCredentialHelper) Get(serverURL string) (string, string, error) {
182+
return d.cache.Get(serverURL)
183+
}
184+
185+
// List is not implemented as it's not needed for our use case
186+
func (d *DockerCredentialHelper) List() (map[string]string, error) {
187+
return map[string]string{}, nil
188+
}

0 commit comments

Comments
 (0)