Files

283 lines
7.5 KiB
Go

package testcontainers
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"sync"
"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/registry"
"github.com/testcontainers/testcontainers-go/internal/core"
)
// defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values.
var defaultRegistryFn = defaultRegistry
// getRegistryCredentials is a variable overwritten in tests to mock the dockercfg.GetRegistryCredentials function.
var getRegistryCredentials = dockercfg.GetRegistryCredentials
// DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
// Finally, it will use the credential helpers to extract the information from the docker config file
// for that registry, if it exists.
func DockerImageAuth(ctx context.Context, image string) (string, registry.AuthConfig, error) {
configs, err := getDockerAuthConfigs()
if err != nil {
reg := core.ExtractRegistry(image, defaultRegistryFn(ctx))
return reg, registry.AuthConfig{}, err
}
return dockerImageAuth(ctx, image, configs)
}
// dockerImageAuth returns the auth config for the given Docker image.
func dockerImageAuth(ctx context.Context, image string, configs map[string]registry.AuthConfig) (string, registry.AuthConfig, error) {
defaultRegistry := defaultRegistryFn(ctx)
reg := core.ExtractRegistry(image, defaultRegistry)
if cfg, ok := getRegistryAuth(reg, configs); ok {
return reg, cfg, nil
}
return reg, registry.AuthConfig{}, dockercfg.ErrCredentialsNotFound
}
func getRegistryAuth(reg string, cfgs map[string]registry.AuthConfig) (registry.AuthConfig, bool) {
if cfg, ok := cfgs[reg]; ok {
return cfg, true
}
// fallback match using authentication key host
for k, cfg := range cfgs {
keyURL, err := url.Parse(k)
if err != nil {
continue
}
host := keyURL.Host
if keyURL.Scheme == "" {
// url.Parse: The url may be relative (a path, without a host) [...]
host = keyURL.Path
}
if host == reg {
return cfg, true
}
}
return registry.AuthConfig{}, false
}
// defaultRegistry returns the default registry to use when pulling images
// It will use the docker daemon to get the default registry, returning "https://index.docker.io/v1/" if
// it fails to get the information from the daemon
func defaultRegistry(ctx context.Context) string {
client, err := NewDockerClientWithOpts(ctx)
if err != nil {
return core.IndexDockerIO
}
defer client.Close()
info, err := client.Info(ctx)
if err != nil {
return core.IndexDockerIO
}
return info.IndexServerAddress
}
// authConfigResult is a result looking up auth details for key.
type authConfigResult struct {
key string
cfg registry.AuthConfig
err error
}
// credentialsCache is a cache for registry credentials.
type credentialsCache struct {
entries map[string]credentials
mtx sync.RWMutex
}
// credentials represents the username and password for a registry.
type credentials struct {
username string
password string
}
var creds = &credentialsCache{entries: map[string]credentials{}}
// AuthConfig updates the details in authConfig for the given hostname
// as determined by the details in configKey.
func (c *credentialsCache) AuthConfig(hostname, configKey string, authConfig *registry.AuthConfig) error {
u, p, err := creds.get(hostname, configKey)
if err != nil {
return err
}
if u != "" {
authConfig.Username = u
authConfig.Password = p
} else {
authConfig.IdentityToken = p
}
return nil
}
// get returns the username and password for the given hostname
// as determined by the details in configPath.
// If the username is empty, the password is an identity token.
func (c *credentialsCache) get(hostname, configKey string) (string, string, error) {
key := configKey + ":" + hostname
c.mtx.RLock()
entry, ok := c.entries[key]
c.mtx.RUnlock()
if ok {
return entry.username, entry.password, nil
}
// No entry found, request and cache.
user, password, err := getRegistryCredentials(hostname)
if err != nil {
return "", "", fmt.Errorf("getting credentials for %s: %w", hostname, err)
}
c.mtx.Lock()
c.entries[key] = credentials{username: user, password: password}
c.mtx.Unlock()
return user, password, nil
}
// configKey returns a key to use for caching credentials based on
// the contents of the currently active config.
func configKey(cfg *dockercfg.Config) (string, error) {
h := md5.New()
if err := json.NewEncoder(h).Encode(cfg); err != nil {
return "", fmt.Errorf("encode config: %w", err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// getDockerAuthConfigs returns a map with the auth configs from the docker config file
// using the registry as the key
func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
cfg, err := getDockerConfig()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return map[string]registry.AuthConfig{}, nil
}
return nil, err
}
key, err := configKey(cfg)
if err != nil {
return nil, err
}
size := len(cfg.AuthConfigs) + len(cfg.CredentialHelpers)
cfgs := make(map[string]registry.AuthConfig, size)
results := make(chan authConfigResult, size)
var wg sync.WaitGroup
wg.Add(size)
for k, v := range cfg.AuthConfigs {
go func(k string, v dockercfg.AuthConfig) {
defer wg.Done()
ac := registry.AuthConfig{
Auth: v.Auth,
Email: v.Email,
IdentityToken: v.IdentityToken,
Password: v.Password,
RegistryToken: v.RegistryToken,
ServerAddress: v.ServerAddress,
Username: v.Username,
}
switch {
case ac.Username == "" && ac.Password == "":
// Look up credentials from the credential store.
if err := creds.AuthConfig(k, key, &ac); err != nil {
results <- authConfigResult{err: err}
return
}
case ac.Auth == "":
// Create auth from the username and password encoding.
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
}
results <- authConfigResult{key: k, cfg: ac}
}(k, v)
}
// In the case where the auth field in the .docker/conf.json is empty, and the user has
// credential helpers registered the auth comes from there.
for k := range cfg.CredentialHelpers {
go func(k string) {
defer wg.Done()
var ac registry.AuthConfig
if err := creds.AuthConfig(k, key, &ac); err != nil {
results <- authConfigResult{err: err}
return
}
results <- authConfigResult{key: k, cfg: ac}
}(k)
}
go func() {
wg.Wait()
close(results)
}()
var errs []error
for result := range results {
if result.err != nil {
errs = append(errs, result.err)
continue
}
cfgs[result.key] = result.cfg
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return cfgs, nil
}
// getDockerConfig returns the docker config file. It will internally check, in this particular order:
// 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a dockercfg.Config
// 2. the DOCKER_CONFIG environment variable, as the path to the config file
// 3. else it will load the default config file, which is ~/.docker/config.json
func getDockerConfig() (*dockercfg.Config, error) {
if env := os.Getenv("DOCKER_AUTH_CONFIG"); env != "" {
var cfg dockercfg.Config
if err := json.Unmarshal([]byte(env), &cfg); err != nil {
return nil, fmt.Errorf("unmarshal DOCKER_AUTH_CONFIG: %w", err)
}
return &cfg, nil
}
cfg, err := dockercfg.LoadDefaultConfig()
if err != nil {
return nil, fmt.Errorf("load default config: %w", err)
}
return &cfg, nil
}