Files

106 lines
2.9 KiB
Go

package updater
import (
"encoding/base64"
"encoding/json"
"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, fmt.Errorf("%s: auth for %s is not user:password", path, key)
}
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)]
}