diff --git a/cmd/buildctl/build.go b/cmd/buildctl/build.go index f31773401c53..def59db1238f 100644 --- a/cmd/buildctl/build.go +++ b/cmd/buildctl/build.go @@ -189,8 +189,8 @@ func buildAction(clicontext *cli.Context) error { } attachable := []session.Attachable{authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{ - ConfigFile: dockerConfig, - TLSConfigs: tlsConfigs, + AuthConfigProvider: authprovider.LoadAuthConfig(dockerConfig), + TLSConfigs: tlsConfigs, })} if ssh := clicontext.StringSlice("ssh"); len(ssh) > 0 { diff --git a/session/auth/authprovider/authconfigprovider.go b/session/auth/authprovider/authconfigprovider.go new file mode 100644 index 000000000000..612eda4260a1 --- /dev/null +++ b/session/auth/authprovider/authconfigprovider.go @@ -0,0 +1,58 @@ +package authprovider + +import ( + "context" + "sync" + "time" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/types" +) + +func LoadAuthConfig(config *configfile.ConfigFile) AuthConfigProvider { + acp := &authConfigProvider{ + config: config, + authConfigCache: map[string]authConfigCacheEntry{}, + } + return acp.load +} + +type authConfigProvider struct { + config *configfile.ConfigFile + authConfigCache map[string]authConfigCacheEntry + mu sync.Mutex +} + +func (ap *authConfigProvider) load(ctx context.Context, host string, scopes []string, cacheExpireCheck ExpireCachedAuthCheck) (types.AuthConfig, error) { + ap.mu.Lock() + defer ap.mu.Unlock() + + entry, exists := ap.authConfigCache[host] + if exists && (cacheExpireCheck == nil || !cacheExpireCheck(entry.Created, host)) { + return *entry.Auth, nil + } + + hostKey := host + if host == DockerHubRegistryHost { + hostKey = DockerHubConfigfileKey + } + + ac, err := ap.config.GetAuthConfig(hostKey) + if err != nil { + return types.AuthConfig{}, err + } + + entry = authConfigCacheEntry{ + Created: time.Now(), + Auth: &ac, + } + + ap.authConfigCache[host] = entry + + return ac, nil +} + +type authConfigCacheEntry struct { + Created time.Time + Auth *types.AuthConfig +} diff --git a/session/auth/authprovider/authprovider.go b/session/auth/authprovider/authprovider.go index 724f172e6701..4ca5a6457742 100644 --- a/session/auth/authprovider/authprovider.go +++ b/session/auth/authprovider/authprovider.go @@ -19,7 +19,6 @@ import ( authutil "github.com/containerd/containerd/v2/core/remotes/docker/auth" remoteserrors "github.com/containerd/containerd/v2/core/remotes/errors" "github.com/docker/cli/cli/config" - "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/types" cleanhttp "github.com/hashicorp/go-cleanhttp" "github.com/moby/buildkit/session" @@ -36,13 +35,17 @@ import ( const ( defaultExpiration = 60 - dockerHubConfigfileKey = "https://index.docker.io/v1/" - dockerHubRegistryHost = "registry-1.docker.io" + DockerHubConfigfileKey = "https://index.docker.io/v1/" + DockerHubRegistryHost = "registry-1.docker.io" ) +type AuthConfigProvider func(ctx context.Context, host string, scope []string, cacheCheck ExpireCachedAuthCheck) (types.AuthConfig, error) + +type ExpireCachedAuthCheck func(created time.Time, serverURL string) bool + type DockerAuthProviderConfig struct { - // ConfigFile is the docker config file - ConfigFile *configfile.ConfigFile + // AuthConfigProvider is a function that provides auth config for a given host and scope + AuthConfigProvider AuthConfigProvider // 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 @@ -50,12 +53,7 @@ type DockerAuthProviderConfig struct { // If nil then the cached result will expire after 4 minutes and 50 seconds. // 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 + ExpireCachedAuth ExpireCachedAuthCheck } func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable { @@ -66,23 +64,21 @@ func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable { } } return &authProvider{ - authConfigCache: map[string]authConfigCacheEntry{}, - expireAc: cfg.ExpireCachedAuth, - config: cfg.ConfigFile, - seeds: &tokenSeeds{dir: config.Dir()}, - loggerCache: map[string]struct{}{}, - tlsConfigs: cfg.TLSConfigs, + expireAc: cfg.ExpireCachedAuth, + provider: cfg.AuthConfigProvider, + seeds: &tokenSeeds{dir: config.Dir()}, + loggerCache: map[string]struct{}{}, + tlsConfigs: cfg.TLSConfigs, } } type authProvider struct { - authConfigCache map[string]authConfigCacheEntry - expireAc func(time.Time, string) bool - config *configfile.ConfigFile - seeds *tokenSeeds - logger progresswriter.Logger - loggerCache map[string]struct{} - tlsConfigs map[string]*AuthTLSConfig + expireAc func(time.Time, string) bool + provider AuthConfigProvider + seeds *tokenSeeds + logger progresswriter.Logger + loggerCache map[string]struct{} + tlsConfigs map[string]*AuthTLSConfig // The need for this mutex is not well understood. // Without it, the docker cli on OS X hangs when @@ -102,7 +98,7 @@ func (ap *authProvider) Register(server *grpc.Server) { } func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequest) (rr *auth.FetchTokenResponse, err error) { - ac, err := ap.getAuthConfig(ctx, req.Host) + ac, err := ap.getAuthConfig(ctx, req.Host, req.Scopes) if err != nil { return nil, err } @@ -112,11 +108,7 @@ func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequ return toTokenResponse(ac.RegistryToken, time.Time{}, 0), nil } - creds, err := ap.credentials(ctx, req.Host) - if err != nil { - return nil, err - } - + creds := toCredentials(*ac) to := authutil.TokenOptions{ Realm: req.Realm, Service: req.Service, @@ -215,11 +207,7 @@ func (ap *authProvider) tlsConfig(host string) (*tls.Config, error) { return tc, nil } -func (ap *authProvider) credentials(ctx context.Context, host string) (*auth.CredentialsResponse, error) { - ac, err := ap.getAuthConfig(ctx, host) - if err != nil { - return nil, err - } +func toCredentials(ac types.AuthConfig) *auth.CredentialsResponse { res := &auth.CredentialsResponse{} if ac.IdentityToken != "" { res.Secret = ac.IdentityToken @@ -227,12 +215,16 @@ func (ap *authProvider) credentials(ctx context.Context, host string) (*auth.Cre res.Username = ac.Username res.Secret = ac.Password } - return res, nil + return res } func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) { - resp, err := ap.credentials(ctx, req.Host) - if err != nil || resp.Secret != "" { + ac, err := ap.getAuthConfig(ctx, req.Host, nil) + if err != nil { + return nil, err + } + resp := toCredentials(*ac) + if resp.Secret != "" { ap.mu.Lock() defer ap.mu.Unlock() _, ok := ap.loggerCache[req.Host] @@ -267,33 +259,22 @@ func (ap *authProvider) VerifyTokenAuthority(ctx context.Context, req *auth.Veri return &auth.VerifyTokenAuthorityResponse{Signed: sign.Sign(nil, req.Payload, priv)}, nil } -func (ap *authProvider) getAuthConfig(ctx context.Context, host string) (*types.AuthConfig, error) { +func (ap *authProvider) getAuthConfig(ctx context.Context, host string, scopes []string) (*types.AuthConfig, error) { ap.mu.Lock() defer ap.mu.Unlock() - if host == dockerHubRegistryHost { - host = dockerHubConfigfileKey - } - - entry, exists := ap.authConfigCache[host] - if exists && !ap.expireAc(entry.Created, host) { - return entry.Auth, 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, + var ac types.AuthConfig + if ap.provider != nil { + span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host)) + res, err := ap.provider(ctx, host, scopes, ap.expireAc) + tracing.FinishWithError(span, err) + if err != nil { + return nil, err + } + ac = res } - ap.authConfigCache[host] = entry - - return entry.Auth, nil + return &ac, nil } func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt []byte) (ed25519.PrivateKey, error) { @@ -301,10 +282,12 @@ func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt [ return nil, status.Errorf(codes.Unavailable, "client side tokens disabled") } - creds, err := ap.credentials(ctx, host) + ac, err := ap.getAuthConfig(ctx, host, nil) if err != nil { return nil, err } + + creds := toCredentials(*ac) seed, err := ap.seeds.getSeed(host) if err != nil { return nil, err diff --git a/session/auth/authprovider/authprovider_test.go b/session/auth/authprovider/authprovider_test.go index 47e3c74e6ca2..260eb9a37d02 100644 --- a/session/auth/authprovider/authprovider_test.go +++ b/session/auth/authprovider/authprovider_test.go @@ -16,21 +16,21 @@ func TestFetchTokenCaching(t *testing.T) { newCfg := func() *configfile.ConfigFile { return &configfile.ConfigFile{ AuthConfigs: map[string]types.AuthConfig{ - dockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"}, + DockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"}, }, } } cfg := newCfg() p := NewDockerAuthProvider(DockerAuthProviderConfig{ - ConfigFile: cfg, + AuthConfigProvider: LoadAuthConfig(cfg), }).(*authProvider) - res, err := p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost}) + 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}) + 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 cached the result instead of returning hunter3. @@ -40,19 +40,19 @@ func TestFetchTokenCaching(t *testing.T) { cfg = newCfg() p = NewDockerAuthProvider(DockerAuthProviderConfig{ - ConfigFile: cfg, + AuthConfigProvider: LoadAuthConfig(cfg), ExpireCachedAuth: func(_ time.Time, host string) bool { - require.Equal(t, dockerHubConfigfileKey, host) + require.Equal(t, DockerHubRegistryHost, host) return true }, }).(*authProvider) - res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost}) + 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}) + 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.