Skip to content

Commit 37e283c

Browse files
committed
auth: add option to load docker config for specific repo/scope
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
1 parent c3514fe commit 37e283c

5 files changed

Lines changed: 468 additions & 9 deletions

File tree

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)
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)