Initial QSfera import
This commit is contained in:
+290
@@ -0,0 +1,290 @@
|
||||
package testcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cpuguy83/dockercfg"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
|
||||
"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)
|
||||
|
||||
// Normalize Docker Hub aliases for credential lookup
|
||||
if strings.EqualFold(reg, "docker.io") ||
|
||||
strings.EqualFold(reg, "registry.hub.docker.com") ||
|
||||
strings.EqualFold(reg, "registry-1.docker.io") {
|
||||
reg = defaultRegistry // This is https://index.docker.io/v1/
|
||||
}
|
||||
|
||||
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 {
|
||||
apiClient, err := NewDockerClientWithOpts(ctx)
|
||||
if err != nil {
|
||||
return core.IndexDockerIO
|
||||
}
|
||||
defer apiClient.Close()
|
||||
|
||||
info, err := apiClient.Info(ctx, client.InfoOptions{})
|
||||
if err != nil {
|
||||
return core.IndexDockerIO
|
||||
}
|
||||
|
||||
return info.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,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user