From 150e7985b625216ab160309793d3c62ba2ad4c1f Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 27 Jan 2025 11:50:11 -0800 Subject: [PATCH] authprovider: Add support for expiring auth cache This exposes a mechanism to expire cached auth configs. Signed-off-by: Brian Goff --- cmd/buildctl/build.go | 6 +- session/auth/authprovider/authprovider.go | 61 ++++++++++++++----- .../auth/authprovider/authprovider_test.go | 39 ++++++++++-- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/cmd/buildctl/build.go b/cmd/buildctl/build.go index a6ca8feb4f15..fd6a4bf0d8b7 100644 --- a/cmd/buildctl/build.go +++ b/cmd/buildctl/build.go @@ -187,7 +187,11 @@ func buildAction(clicontext *cli.Context) error { if err != nil { return err } - attachable := []session.Attachable{authprovider.NewDockerAuthProvider(dockerConfig, tlsConfigs)} + + attachable := []session.Attachable{authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{ + ConfigFile: dockerConfig, + TLSConfigs: tlsConfigs, + })} if ssh := clicontext.StringSlice("ssh"); len(ssh) > 0 { configs, err := build.ParseSSH(ssh) diff --git a/session/auth/authprovider/authprovider.go b/session/auth/authprovider/authprovider.go index de3a7daef6fc..0280e96dc9ad 100644 --- a/session/auth/authprovider/authprovider.go +++ b/session/auth/authprovider/authprovider.go @@ -38,18 +38,43 @@ const ( dockerHubRegistryHost = "registry-1.docker.io" ) -func NewDockerAuthProvider(cfg *configfile.ConfigFile, tlsConfigs map[string]*AuthTLSConfig) session.Attachable { +type DockerAuthProviderConfig struct { + // ConfigFile is the docker config file + ConfigFile *configfile.ConfigFile + // TLSConfigs is a map of host to TLS config + TLSConfigs map[string]*AuthTLSConfig + // ExpireCachedAuth is a function that returns true auth config should be refreshed + // instead of using a pre-cached result. + // If nil then the cached result will expire after 10 minutes. + // The function is called with the time the cached auth config was created + // and the server URL the auth config is for. + ExpireCachedAuth func(created time.Time, serverURL string) bool +} + +type authConfigCacheEntry struct { + Created time.Time + Auth *types.AuthConfig +} + +func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable { + if cfg.ExpireCachedAuth == nil { + cfg.ExpireCachedAuth = func(created time.Time, _ string) bool { + return time.Since(created) > 10*time.Minute + } + } return &authProvider{ - authConfigCache: map[string]*types.AuthConfig{}, - config: cfg, + authConfigCache: map[string]authConfigCacheEntry{}, + expireAc: cfg.ExpireCachedAuth, + config: cfg.ConfigFile, seeds: &tokenSeeds{dir: config.Dir()}, loggerCache: map[string]struct{}{}, - tlsConfigs: tlsConfigs, + tlsConfigs: cfg.TLSConfigs, } } type authProvider struct { - authConfigCache map[string]*types.AuthConfig + authConfigCache map[string]authConfigCacheEntry + expireAc func(time.Time, string) bool config *configfile.ConfigFile seeds *tokenSeeds logger progresswriter.Logger @@ -247,17 +272,25 @@ func (ap *authProvider) getAuthConfig(ctx context.Context, host string) (*types. host = dockerHubConfigfileKey } - if _, exists := ap.authConfigCache[host]; !exists { - span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host)) - ac, err := ap.config.GetAuthConfig(host) - tracing.FinishWithError(span, err) - if err != nil { - return nil, err - } - ap.authConfigCache[host] = &ac + entry, exists := ap.authConfigCache[host] + if exists && !ap.expireAc(entry.Created, host) { + return entry.Auth, nil } - return ap.authConfigCache[host], nil + span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host)) + ac, err := ap.config.GetAuthConfig(host) + tracing.FinishWithError(span, err) + if err != nil { + return nil, err + } + entry = authConfigCacheEntry{ + Created: time.Now(), + Auth: &ac, + } + + ap.authConfigCache[host] = entry + + return entry.Auth, nil } func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt []byte) (ed25519.PrivateKey, error) { diff --git a/session/auth/authprovider/authprovider_test.go b/session/auth/authprovider/authprovider_test.go index 69e67f8821c7..47e3c74e6ca2 100644 --- a/session/auth/authprovider/authprovider_test.go +++ b/session/auth/authprovider/authprovider_test.go @@ -3,6 +3,7 @@ package authprovider import ( "context" "testing" + "time" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/types" @@ -12,12 +13,18 @@ import ( ) func TestFetchTokenCaching(t *testing.T) { - cfg := &configfile.ConfigFile{ - AuthConfigs: map[string]types.AuthConfig{ - dockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"}, - }, + newCfg := func() *configfile.ConfigFile { + return &configfile.ConfigFile{ + AuthConfigs: map[string]types.AuthConfig{ + dockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"}, + }, + } } - p := NewDockerAuthProvider(cfg, nil).(*authProvider) + + cfg := newCfg() + p := NewDockerAuthProvider(DockerAuthProviderConfig{ + ConfigFile: cfg, + }).(*authProvider) res, err := p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost}) require.NoError(t, err) assert.Equal(t, "hunter2", res.Token) @@ -28,4 +35,26 @@ func TestFetchTokenCaching(t *testing.T) { // Verify that we cached the result instead of returning hunter3. assert.Equal(t, "hunter2", res.Token) + + // Now again but this time expire the auth. + + cfg = newCfg() + p = NewDockerAuthProvider(DockerAuthProviderConfig{ + ConfigFile: cfg, + ExpireCachedAuth: func(_ time.Time, host string) bool { + require.Equal(t, dockerHubConfigfileKey, host) + return true + }, + }).(*authProvider) + + res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost}) + require.NoError(t, err) + assert.Equal(t, "hunter2", res.Token) + + cfg.AuthConfigs[dockerHubConfigfileKey] = types.AuthConfig{Username: "user", RegistryToken: "hunter3"} + res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost}) + require.NoError(t, err) + + // Verify that we re-fetched the token after it expired. + assert.Equal(t, "hunter3", res.Token) }