Files
buildx/util/dockerutil/dockerconfig/configprovider.go
T
Tonis Tiigi 27dde04ab5 imagetools: use dockerconfig for auth
Enables fallback for DHI and Scout registries and
repo/scope specific credentials like supported for builds.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-01-26 10:15:14 -08:00

289 lines
6.5 KiB
Go

package dockerconfig
import (
"cmp"
"context"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
"github.com/docker/buildx/util/confutil"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/moby/buildkit/session/auth/authprovider"
)
const (
dockerHubRegistryHost = "registry-1.docker.io"
dockerDHIRegistryHost = "dhi.io"
dockerScoutRegistryHost = "registry.scout.docker.com"
)
func LoadAuthConfig(cli command.Cli) authprovider.AuthConfigProvider {
acp := &authConfigProvider{
buildxConfig: confutil.NewConfig(cli),
defaultConfig: cli.ConfigFile(),
authConfigCache: map[string]authConfigCacheEntry{},
}
return acp.load
}
type authConfigProvider struct {
initOnce sync.Once
defaultConfig *configfile.ConfigFile
buildxConfig *confutil.Config
authConfigCache map[string]authConfigCacheEntry
mu sync.Mutex // mutex for authConfigCache
alternativeConfigs []*alternativeConfig
}
func (ap *authConfigProvider) load(ctx context.Context, host string, scopes []string, cacheExpireCheck authprovider.ExpireCachedAuthCheck) (types.AuthConfig, error) {
if cacheExpireCheck == nil {
cacheExpireCheck = func(created time.Time, _ string) bool {
// Tokens for Google Artifact Registry via Workload Identity expire after 5 minutes.
return time.Since(created) > 4*time.Minute+50*time.Second
}
}
ac, err := ap.loadHost(ctx, host, scopes, cacheExpireCheck)
if err != nil {
return types.AuthConfig{}, err
}
if ac == (types.AuthConfig{}) {
// DHI and Scout are also Hub backed registries by Docker, fallback if no specific auth found
switch host {
case dockerDHIRegistryHost, dockerScoutRegistryHost:
ac, err := ap.loadHost(ctx, dockerHubRegistryHost, scopes, cacheExpireCheck)
if err != nil {
return types.AuthConfig{}, nil
}
return ac, nil
default:
return types.AuthConfig{}, err
}
}
return ac, nil
}
func (ap *authConfigProvider) loadHost(_ context.Context, host string, scopes []string, cacheExpireCheck authprovider.ExpireCachedAuthCheck) (types.AuthConfig, error) {
ap.initOnce.Do(func() {
ap.init()
})
ap.mu.Lock()
defer ap.mu.Unlock()
candidates := []*alternativeConfig{}
parsedScopes := parseScopes(scopes)
if len(parsedScopes) == 1 {
for _, cfg := range ap.alternativeConfigs {
if cfg.host != host {
continue
}
if cfg.matchesScopes(parsedScopes) {
candidates = append(candidates, cfg)
}
}
}
key := host
cfg := ap.defaultConfig
if len(candidates) > 0 {
// matches with repo before those without repo
// matches with scope set sorted before those without scope
slices.SortFunc(candidates, func(a, b *alternativeConfig) int {
return cmp.Or(
strings.Compare(b.repo, a.repo),
cmp.Compare(len(b.scope), len(a.scope)),
)
})
candidates = candidates[:1]
key += "|" + candidates[0].dir
if candidates[0].configFile == nil {
if cfgDir, err := config.Load(candidates[0].dir); err == nil {
cfg = cfgDir
candidates[0].configFile = cfg
}
} else {
cfg = candidates[0].configFile
}
}
entry, exists := ap.authConfigCache[key]
if exists && (cacheExpireCheck == nil || !cacheExpireCheck(entry.Created, key)) {
return *entry.Auth, nil
}
hostKey := host
if host == authprovider.DockerHubRegistryHost {
hostKey = authprovider.DockerHubConfigfileKey
}
ac, err := cfg.GetAuthConfig(hostKey)
if err != nil {
return types.AuthConfig{}, err
}
entry = authConfigCacheEntry{
Created: time.Now(),
Auth: &ac,
}
ap.authConfigCache[key] = entry
return ac, nil
}
func (ap *authConfigProvider) init() error {
base := filepath.Join(ap.buildxConfig.Dir(), "config")
return filepath.WalkDir(base, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if d.Name() != config.ConfigFileName {
return nil
}
dir := filepath.Dir(path)
rdir, err := filepath.Rel(base, dir)
if err != nil {
return err
}
cfg := parseConfigKey(rdir)
cfg.dir = dir
ap.alternativeConfigs = append(ap.alternativeConfigs, &cfg)
return nil
})
}
func parseConfigKey(key string) alternativeConfig {
var out alternativeConfig
var mainPart, scopePart string
if before, after, ok := strings.Cut(key, "@"); ok {
mainPart = before
scopePart = after
} else {
mainPart = key
}
if scopePart != "" {
out.scope = make(map[string]struct{})
for s := range strings.SplitSeq(scopePart, ",") {
if s = strings.TrimSpace(s); s != "" {
out.scope[s] = struct{}{}
}
}
}
if mainPart == "" {
return out
}
before, after, ok := strings.Cut(mainPart, "/")
if !ok {
out.host = mainPart
return out
}
out.host = before
out.repo = after
return out
}
type alternativeConfig struct {
dir string
host string
repo string
scope map[string]struct{}
configFile *configfile.ConfigFile
}
func (a *alternativeConfig) matchesScopes(q scopes) bool {
if a.repo != "" {
if _, ok := q["repository:"+a.repo]; !ok {
return false
}
}
if len(a.scope) > 0 {
if a.repo == "" {
// no repo means one query must match all scopes
for _, scopeActions := range q {
ok := true
for s := range a.scope {
if _, exists := scopeActions[s]; !exists {
ok = false
break
}
}
if ok {
return true
}
}
return false
}
for s := range a.scope {
for k, scopeActions := range q {
if k == "repository:"+a.repo {
if _, ok := scopeActions[s]; !ok {
return false
}
}
}
}
}
return true
}
type authConfigCacheEntry struct {
Created time.Time
Auth *types.AuthConfig
}
type scopes map[string]map[string]struct{}
func parseScopes(s []string) scopes {
// https://distribution.github.io/distribution/spec/auth/scope/
m := map[string]map[string]struct{}{}
for _, scopeStr := range s {
if scopeStr == "" {
return nil
}
// The scopeStr may have strings that contain multiple scopes separated by a space.
for scope := range strings.SplitSeq(scopeStr, " ") {
parts := strings.SplitN(scope, ":", 3)
names := []string{parts[0]}
if len(parts) > 1 {
names = append(names, parts[1])
}
var actions []string
if len(parts) == 3 {
actions = append(actions, strings.Split(parts[2], ",")...)
}
name := strings.Join(names, ":")
ma, ok := m[name]
if !ok {
ma = map[string]struct{}{}
m[name] = ma
}
for _, a := range actions {
ma[a] = struct{}{}
}
}
}
return m
}