Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/buildctl/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
58 changes: 58 additions & 0 deletions session/auth/authprovider/authconfigprovider.go
Original file line number Diff line number Diff line change
@@ -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
}
105 changes: 44 additions & 61 deletions session/auth/authprovider/authprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -36,26 +35,25 @@ 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
// instead of using a pre-cached result.
// 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 {
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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,
Expand Down Expand Up @@ -215,24 +207,24 @@ 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
} else {
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]
Expand Down Expand Up @@ -267,44 +259,35 @@ 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) {
if v, err := strconv.ParseBool(os.Getenv("BUILDKIT_NO_CLIENT_TOKEN")); err == nil && v {
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
Expand Down
20 changes: 10 additions & 10 deletions session/auth/authprovider/authprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines 25 to 27
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not for this PR but maybe we could move the docker auth provider to another package like session/auth/authprovider/dockerconfig?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can do that in a follow-up

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.
Expand All @@ -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.
Expand Down