mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-30 09:38:26 -05:00
283 lines
7.5 KiB
Go
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
|
|
}
|