Skip to content

Commit 8037f19

Browse files
authored
Merge pull request #3562 from tonistiigi/docker-config-scope-workaround
auth: add option to load docker config for specific repo/scope
2 parents 752e0b2 + 37e283c commit 8037f19

File tree

26 files changed

+980
-10489
lines changed

26 files changed

+980
-10489
lines changed

bake/bake.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@ import (
2323
"github.com/docker/buildx/util/buildflags"
2424
"github.com/docker/buildx/util/platformutil"
2525
"github.com/docker/buildx/util/progress"
26-
"github.com/docker/cli/cli/config"
2726
dockeropts "github.com/docker/cli/opts"
2827
hcl "github.com/hashicorp/hcl/v2"
2928
"github.com/moby/buildkit/client"
3029
"github.com/moby/buildkit/client/llb"
31-
"github.com/moby/buildkit/session/auth/authprovider"
3230
"github.com/pkg/errors"
3331
"github.com/zclconf/go-cty/cty"
3432
"github.com/zclconf/go-cty/cty/convert"
@@ -1256,18 +1254,12 @@ func (t *Target) GetName(ectx *hcl.EvalContext, block *hcl.Block, loadDeps func(
12561254
}
12571255

12581256
func TargetsToBuildOpt(m map[string]*Target, inp *Input) (map[string]build.Options, error) {
1259-
// make sure local credentials are loaded multiple times for different targets
1260-
authProvider := authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
1261-
ConfigFile: config.LoadDefaultConfigFile(os.Stderr),
1262-
})
1263-
12641257
m2 := make(map[string]build.Options, len(m))
12651258
for k, v := range m {
12661259
bo, err := toBuildOpt(v, inp)
12671260
if err != nil {
12681261
return nil, err
12691262
}
1270-
bo.Session = append(bo.Session, authProvider)
12711263
m2[k] = *bo
12721264
}
12731265
return m2, nil

commands/bake.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import (
2929
"github.com/docker/buildx/util/confutil"
3030
"github.com/docker/buildx/util/desktop"
3131
"github.com/docker/buildx/util/dockerutil"
32+
"github.com/docker/buildx/util/dockerutil/dockerconfig"
3233
"github.com/docker/buildx/util/osutil"
3334
"github.com/docker/buildx/util/progress"
3435
"github.com/docker/buildx/util/tracing"
3536
"github.com/docker/cli/cli/command"
3637
"github.com/moby/buildkit/identity"
38+
"github.com/moby/buildkit/session/auth/authprovider"
3739
"github.com/moby/buildkit/util/progress/progressui"
3840
"github.com/pkg/errors"
3941
"github.com/spf13/cobra"
@@ -252,6 +254,16 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
252254
return err
253255
}
254256

257+
// make sure local credentials aren't loaded multiple times for different targets
258+
authProvider := authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
259+
AuthConfigProvider: dockerconfig.LoadAuthConfig(dockerCli),
260+
})
261+
262+
for k, opt := range bo {
263+
opt.Session = append(opt.Session, authProvider)
264+
bo[k] = opt
265+
}
266+
255267
def := struct {
256268
Group map[string]*bake.Group `json:"group,omitempty"`
257269
Target map[string]*bake.Target `json:"target"`

commands/build.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/docker/buildx/util/confutil"
2828
"github.com/docker/buildx/util/desktop"
2929
"github.com/docker/buildx/util/dockerutil"
30+
"github.com/docker/buildx/util/dockerutil/dockerconfig"
3031
"github.com/docker/buildx/util/ioset"
3132
"github.com/docker/buildx/util/metricutil"
3233
"github.com/docker/buildx/util/osutil"
@@ -1019,7 +1020,7 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
10191020
opts.Platforms = platforms
10201021

10211022
opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
1022-
ConfigFile: dockerCli.ConfigFile(),
1023+
AuthConfigProvider: dockerconfig.LoadAuthConfig(dockerCli),
10231024
}))
10241025

10251026
secrets, err := build.CreateSecrets(in.Secrets)

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ require (
2929
github.com/hashicorp/hcl/v2 v2.24.0
3030
github.com/in-toto/in-toto-golang v0.9.0
3131
github.com/mitchellh/hashstructure/v2 v2.0.2
32-
github.com/moby/buildkit v0.26.2
33-
github.com/moby/go-archive v0.1.0
32+
github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40 // v0.27.0-dev
33+
github.com/moby/go-archive v0.2.0
3434
github.com/moby/moby/api v1.52.0
3535
github.com/moby/moby/client v0.2.1
3636
github.com/moby/sys/atomicwriter v0.1.0
@@ -47,7 +47,7 @@ require (
4747
github.com/spf13/cobra v1.10.2
4848
github.com/spf13/pflag v1.0.10
4949
github.com/stretchr/testify v1.11.1
50-
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f
50+
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f
5151
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0
5252
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117
5353
github.com/zclconf/go-cty v1.17.0

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,12 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
210210
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
211211
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
212212
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
213-
github.com/moby/buildkit v0.26.2 h1:EIh5j0gzRsCZmQzvgNNWzSDbuKqwUIiBH7ssqLv8RU8=
214-
github.com/moby/buildkit v0.26.2/go.mod h1:ylDa7IqzVJgLdi/wO7H1qLREFQpmhFbw2fbn4yoTw40=
213+
github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40 h1:h45o8dTo7Cq1Su49bs9VHP83Fdp3olgMfgo3tZULXUw=
214+
github.com/moby/buildkit v0.26.1-0.20260106154623-ed6dc749ce40/go.mod h1:LtUhyzVLAD0ilniDZRbghEyRq9CgZAY7ywMXlVo9MBI=
215215
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
216216
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
217-
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
218-
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
217+
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
218+
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
219219
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
220220
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
221221
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
@@ -319,8 +319,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
319319
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
320320
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
321321
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
322-
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f h1:MoxeMfHAe5Qj/ySSBfL8A7l1V+hxuluj8owsIEEZipI=
323-
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
322+
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f h1:Z4NEQ86qFl1mHuCu9gwcE+EYCwDKfXAYXZbdIXyxmEA=
323+
github.com/tonistiigi/fsutil v0.0.0-20251211185533-a2aa163d723f/go.mod h1:BKdcez7BiVtBvIcef90ZPc6ebqIWr4JWD7+EvLm6J98=
324324
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE=
325325
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
326326
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250408171107-3dd17559e117 h1:XFwyh2JZwR5aiKLXHX2C1n0v5F11dCJpyGL1W/Cpl3U=
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package dockerconfig
2+
3+
import (
4+
"cmp"
5+
"context"
6+
"os"
7+
"path/filepath"
8+
"slices"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/docker/buildx/util/confutil"
14+
"github.com/docker/cli/cli/command"
15+
"github.com/docker/cli/cli/config"
16+
"github.com/docker/cli/cli/config/configfile"
17+
"github.com/docker/cli/cli/config/types"
18+
"github.com/moby/buildkit/session/auth/authprovider"
19+
)
20+
21+
func LoadAuthConfig(cli command.Cli) authprovider.AuthConfigProvider {
22+
acp := &authConfigProvider{
23+
buildxConfig: confutil.NewConfig(cli),
24+
defaultConfig: cli.ConfigFile(),
25+
authConfigCache: map[string]authConfigCacheEntry{},
26+
}
27+
return acp.load
28+
}
29+
30+
type authConfigProvider struct {
31+
initOnce sync.Once
32+
defaultConfig *configfile.ConfigFile
33+
buildxConfig *confutil.Config
34+
authConfigCache map[string]authConfigCacheEntry
35+
mu sync.Mutex // mutex for authConfigCache
36+
alternativeConfigs []*alternativeConfig
37+
}
38+
39+
func (ap *authConfigProvider) load(ctx context.Context, host string, scopes []string, cacheExpireCheck authprovider.ExpireCachedAuthCheck) (types.AuthConfig, error) {
40+
ap.initOnce.Do(func() {
41+
ap.init()
42+
})
43+
44+
ap.mu.Lock()
45+
defer ap.mu.Unlock()
46+
47+
candidates := []*alternativeConfig{}
48+
parsedScopes := parseScopes(scopes)
49+
50+
if len(parsedScopes) == 1 {
51+
for _, cfg := range ap.alternativeConfigs {
52+
if cfg.host != host {
53+
continue
54+
}
55+
if cfg.matchesScopes(parsedScopes) {
56+
candidates = append(candidates, cfg)
57+
}
58+
}
59+
}
60+
key := host
61+
cfg := ap.defaultConfig
62+
if len(candidates) > 0 {
63+
// matches with repo before those without repo
64+
// matches with scope set sorted before those without scope
65+
slices.SortFunc(candidates, func(a, b *alternativeConfig) int {
66+
return cmp.Or(
67+
strings.Compare(b.repo, a.repo),
68+
cmp.Compare(len(b.scope), len(a.scope)),
69+
)
70+
})
71+
candidates = candidates[:1]
72+
key += "|" + candidates[0].dir
73+
if candidates[0].configFile == nil {
74+
if cfgDir, err := config.Load(candidates[0].dir); err == nil {
75+
cfg = cfgDir
76+
candidates[0].configFile = cfg
77+
}
78+
} else {
79+
cfg = candidates[0].configFile
80+
}
81+
}
82+
83+
entry, exists := ap.authConfigCache[key]
84+
if exists && (cacheExpireCheck == nil || !cacheExpireCheck(entry.Created, key)) {
85+
return *entry.Auth, nil
86+
}
87+
88+
hostKey := host
89+
if host == authprovider.DockerHubRegistryHost {
90+
hostKey = authprovider.DockerHubConfigfileKey
91+
}
92+
93+
ac, err := cfg.GetAuthConfig(hostKey)
94+
if err != nil {
95+
return types.AuthConfig{}, err
96+
}
97+
98+
entry = authConfigCacheEntry{
99+
Created: time.Now(),
100+
Auth: &ac,
101+
}
102+
103+
ap.authConfigCache[key] = entry
104+
105+
return ac, nil
106+
}
107+
108+
func (ap *authConfigProvider) init() error {
109+
base := filepath.Join(ap.buildxConfig.Dir(), "config")
110+
return filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
111+
if err != nil {
112+
return err
113+
}
114+
if d.IsDir() {
115+
return nil
116+
}
117+
if d.Name() != config.ConfigFileName {
118+
return nil
119+
}
120+
dir := filepath.Dir(path)
121+
rdir, err := filepath.Rel(base, dir)
122+
if err != nil {
123+
return err
124+
}
125+
cfg := parseConfigKey(rdir)
126+
cfg.dir = dir
127+
ap.alternativeConfigs = append(ap.alternativeConfigs, &cfg)
128+
return nil
129+
})
130+
}
131+
132+
func parseConfigKey(key string) alternativeConfig {
133+
var out alternativeConfig
134+
135+
var mainPart, scopePart string
136+
if i := strings.IndexByte(key, '@'); i >= 0 {
137+
mainPart = key[:i]
138+
scopePart = key[i+1:]
139+
} else {
140+
mainPart = key
141+
}
142+
143+
if scopePart != "" {
144+
out.scope = make(map[string]struct{})
145+
for s := range strings.SplitSeq(scopePart, ",") {
146+
if s = strings.TrimSpace(s); s != "" {
147+
out.scope[s] = struct{}{}
148+
}
149+
}
150+
}
151+
152+
if mainPart == "" {
153+
return out
154+
}
155+
156+
slash := strings.IndexByte(mainPart, '/')
157+
if slash < 0 {
158+
out.host = mainPart
159+
return out
160+
}
161+
162+
out.host = mainPart[:slash]
163+
out.repo = mainPart[slash+1:]
164+
165+
return out
166+
}
167+
168+
type alternativeConfig struct {
169+
dir string
170+
171+
host string
172+
repo string
173+
scope map[string]struct{}
174+
175+
configFile *configfile.ConfigFile
176+
}
177+
178+
func (a *alternativeConfig) matchesScopes(q scopes) bool {
179+
if a.repo != "" {
180+
if _, ok := q["repository:"+a.repo]; !ok {
181+
return false
182+
}
183+
}
184+
185+
if len(a.scope) > 0 {
186+
if a.repo == "" {
187+
// no repo means one query must match all scopes
188+
for _, scopeActions := range q {
189+
ok := true
190+
for s := range a.scope {
191+
if _, exists := scopeActions[s]; !exists {
192+
ok = false
193+
break
194+
}
195+
}
196+
if ok {
197+
return true
198+
}
199+
}
200+
return false
201+
}
202+
for s := range a.scope {
203+
for k, scopeActions := range q {
204+
if k == "repository:"+a.repo {
205+
if _, ok := scopeActions[s]; !ok {
206+
return false
207+
}
208+
}
209+
}
210+
}
211+
}
212+
213+
return true
214+
}
215+
216+
type authConfigCacheEntry struct {
217+
Created time.Time
218+
Auth *types.AuthConfig
219+
}
220+
221+
type scopes map[string]map[string]struct{}
222+
223+
func parseScopes(s []string) scopes {
224+
// https://distribution.github.io/distribution/spec/auth/scope/
225+
m := map[string]map[string]struct{}{}
226+
for _, scopeStr := range s {
227+
if scopeStr == "" {
228+
return nil
229+
}
230+
// The scopeStr may have strings that contain multiple scopes separated by a space.
231+
for scope := range strings.SplitSeq(scopeStr, " ") {
232+
parts := strings.SplitN(scope, ":", 3)
233+
names := []string{parts[0]}
234+
if len(parts) > 1 {
235+
names = append(names, parts[1])
236+
}
237+
var actions []string
238+
if len(parts) == 3 {
239+
actions = append(actions, strings.Split(parts[2], ",")...)
240+
}
241+
name := strings.Join(names, ":")
242+
ma, ok := m[name]
243+
if !ok {
244+
ma = map[string]struct{}{}
245+
m[name] = ma
246+
}
247+
248+
for _, a := range actions {
249+
ma[a] = struct{}{}
250+
}
251+
}
252+
}
253+
return m
254+
}

0 commit comments

Comments
 (0)