Skip to content

Commit 249d7e1

Browse files
authored
Refresh authn.DefaultKeychain creds every 5 min (#1624)
Sometimes Authenticators stick around for a while and eventually expire. This changes modifies Authenticators returned by DefaultKeychain to re-resolve after 5 minutes to ensure the underlying creds are fresh. The hard-coded 5 minutes should prevent us from spamming the underlying Keychain (it can be expensive) while also preventing the creds from expiring. This also exposes authn.RefreshingKeychain for others to use.
1 parent b8d1c0a commit 249d7e1

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed

pkg/authn/keychain.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"os"
1919
"path/filepath"
2020
"sync"
21+
"time"
2122

2223
"github.com/docker/cli/cli/config"
2324
"github.com/docker/cli/cli/config/configfile"
@@ -52,7 +53,7 @@ type defaultKeychain struct {
5253

5354
var (
5455
// DefaultKeychain implements Keychain by interpreting the docker config file.
55-
DefaultKeychain Keychain = &defaultKeychain{}
56+
DefaultKeychain = RefreshingKeychain(&defaultKeychain{}, 5*time.Minute)
5657
)
5758

5859
const (
@@ -178,3 +179,71 @@ func (w wrapper) Resolve(r Resource) (Authenticator, error) {
178179
}
179180
return FromConfig(AuthConfig{Username: u, Password: p}), nil
180181
}
182+
183+
func RefreshingKeychain(inner Keychain, duration time.Duration) Keychain {
184+
return &refreshingKeychain{
185+
keychain: inner,
186+
duration: duration,
187+
}
188+
}
189+
190+
type refreshingKeychain struct {
191+
keychain Keychain
192+
duration time.Duration
193+
clock func() time.Time
194+
}
195+
196+
func (r *refreshingKeychain) Resolve(target Resource) (Authenticator, error) {
197+
last := time.Now()
198+
auth, err := r.keychain.Resolve(target)
199+
if err != nil || auth == Anonymous {
200+
return auth, err
201+
}
202+
return &refreshing{
203+
target: target,
204+
keychain: r.keychain,
205+
last: last,
206+
cached: auth,
207+
duration: r.duration,
208+
clock: r.clock,
209+
}, nil
210+
}
211+
212+
type refreshing struct {
213+
sync.Mutex
214+
target Resource
215+
keychain Keychain
216+
217+
duration time.Duration
218+
219+
last time.Time
220+
cached Authenticator
221+
222+
// for testing
223+
clock func() time.Time
224+
}
225+
226+
func (r *refreshing) Authorization() (*AuthConfig, error) {
227+
r.Lock()
228+
defer r.Unlock()
229+
if r.cached == nil || r.expired() {
230+
r.last = r.now()
231+
auth, err := r.keychain.Resolve(r.target)
232+
if err != nil {
233+
return nil, err
234+
}
235+
r.cached = auth
236+
}
237+
return r.cached.Authorization()
238+
}
239+
240+
func (r *refreshing) now() time.Time {
241+
if r.clock == nil {
242+
return time.Now()
243+
}
244+
return r.clock()
245+
}
246+
247+
func (r *refreshing) expired() bool {
248+
return r.now().Sub(r.last) > r.duration
249+
}

pkg/authn/keychain_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"path/filepath"
2525
"reflect"
2626
"testing"
27+
"time"
2728

2829
"github.com/google/go-containerregistry/pkg/name"
2930
)
@@ -390,3 +391,55 @@ func TestConfigFileIsADir(t *testing.T) {
390391
t.Errorf("expected Anonymous, got %v", auth)
391392
}
392393
}
394+
395+
type fakeKeychain struct {
396+
auth Authenticator
397+
err error
398+
399+
count int
400+
}
401+
402+
func (k *fakeKeychain) Resolve(target Resource) (Authenticator, error) {
403+
k.count++
404+
return k.auth, k.err
405+
}
406+
407+
func TestRefreshingAuth(t *testing.T) {
408+
repo := name.MustParseReference("example.com/my/repo").Context()
409+
last := time.Now()
410+
411+
// Increments by 1 minute each invocation.
412+
clock := func() time.Time {
413+
last = last.Add(1 * time.Minute)
414+
return last
415+
}
416+
417+
want := AuthConfig{
418+
Username: "foo",
419+
Password: "secret",
420+
}
421+
422+
keychain := &fakeKeychain{FromConfig(want), nil, 0}
423+
rk := RefreshingKeychain(keychain, 5*time.Minute)
424+
rk.(*refreshingKeychain).clock = clock
425+
426+
auth, err := rk.Resolve(repo)
427+
if err != nil {
428+
t.Fatal(err)
429+
}
430+
431+
for i := 0; i < 10; i++ {
432+
got, err := auth.Authorization()
433+
if err != nil {
434+
t.Fatal(err)
435+
}
436+
437+
if *got != want {
438+
t.Errorf("got %+v, want %+v", got, want)
439+
}
440+
}
441+
442+
if got, want := keychain.count, 2; got != want {
443+
t.Errorf("refreshed %d times, wanted %d", got, want)
444+
}
445+
}

0 commit comments

Comments
 (0)