package updater import ( "encoding/base64" "encoding/json" "errors" "fmt" "log/slog" "os" "path/filepath" "strings" "github.com/distribution/reference" "github.com/docker/docker/api/types/registry" ) // RegistryAuth holds inline credentials from a docker config.json, already // encoded as X-Registry-Auth values. A nil *RegistryAuth means anonymous // access. type RegistryAuth struct { byHost map[string]string } type dockerConfigFile struct { Auths map[string]struct { Auth string `json:"auth"` IdentityToken string `json:"identitytoken"` } `json:"auths"` CredsStore string `json:"credsStore"` CredHelpers map[string]string `json:"credHelpers"` } // LoadRegistryAuth reads dockerConfigDir/config.json. An empty dir returns // nil, nil. Only inline `auth` and `identitytoken` entries are supported; // credential helpers are not available inside the agent container, which is // logged once. func LoadRegistryAuth(dockerConfigDir string, log *slog.Logger) (*RegistryAuth, error) { if dockerConfigDir == "" { return nil, nil } path := filepath.Join(dockerConfigDir, "config.json") raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read %s: %w", path, err) } var cfg dockerConfigFile if err := json.Unmarshal(raw, &cfg); err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } if (cfg.CredsStore != "" || len(cfg.CredHelpers) > 0) && log != nil { log.Warn("registry_cred_helpers_unsupported", "path", path) } a := &RegistryAuth{byHost: make(map[string]string)} for key, entry := range cfg.Auths { host := normaliseRegistryKey(key) ac := registry.AuthConfig{ServerAddress: host} switch { case entry.IdentityToken != "": ac.IdentityToken = entry.IdentityToken case entry.Auth != "": dec, err := base64.StdEncoding.DecodeString(entry.Auth) if err != nil { return nil, fmt.Errorf("%s: auth for %s is not base64", path, key) } user, pass, ok := strings.Cut(string(dec), ":") if !ok { return nil, errors.New(path + ": auth for " + key + " is not user:password") } ac.Username, ac.Password = user, pass default: continue } enc, err := registry.EncodeAuthConfig(ac) if err != nil { return nil, fmt.Errorf("encode auth for %s: %w", key, err) } a.byHost[host] = enc } return a, nil } // normaliseRegistryKey reduces a config.json key to a bare registry host, // mapping every Docker Hub spelling to "docker.io". func normaliseRegistryKey(key string) string { k := strings.TrimPrefix(strings.TrimPrefix(key, "https://"), "http://") if i := strings.Index(k, "/"); i >= 0 { k = k[:i] } switch k { case "index.docker.io", "docker.io", "registry-1.docker.io": return "docker.io" } return k } // For returns the X-Registry-Auth value for repo's registry, or "". func (a *RegistryAuth) For(repo string) string { if a == nil { return "" } named, err := reference.ParseNormalizedNamed(repo) if err != nil { return "" } return a.byHost[reference.Domain(named)] }