Skip to content

Commit

Permalink
Merge branch 'main' into v1
Browse files Browse the repository at this point in the history
* main:
  fix: config via environment (#2725)
  fix(redpanda): race condition on port check (#2692)
  fix: logging restart (#2697)
  fix!: docker authentication setup (#2727)
  chore: improve error wrapping (#2720)
  chore: run make tests in verbose mode (#2734)
  chore(deps): bump github.com/docker/docker from 27.1.0+incompatible to 27.1.1+incompatible (#2733)
  fix(kafka): port race on start (#2696)
  docs: fix broken doc tags (#2732)
  fix: nginx request failures (#2723)
  fix(compose): container locking (#2722)
  fix(wait): log test timeout (#2716)
  chore: increase timeout values (#2719)
  chore: remove unused parameters (#2721)
  chore(mockserver): silence warning about internal port (#2730)
  feat(wait): skip internal host port check (#2691)
  • Loading branch information
mdelapenya committed Aug 16, 2024
2 parents 372e67f + c44b1b2 commit 6e6c62c
Show file tree
Hide file tree
Showing 120 changed files with 697 additions and 438 deletions.
159 changes: 132 additions & 27 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ package auth

import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"os"
"sync"
Expand All @@ -17,19 +22,25 @@ import (
// defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values.
var defaultRegistryFn = defaultRegistry

// DockerForDockerImageImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
// ForDockerImage 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 ForDockerImage(ctx context.Context, img string) (string, registry.AuthConfig, error) {
defaultRegistry := defaultRegistryFn(ctx)
reg := core.ExtractRegistry(img, defaultRegistry)

cfgs, err := getDockerAuthConfigs()
configs, err := GetDockerConfigs()
if err != nil {
reg := core.ExtractRegistry(img, defaultRegistryFn(ctx))
return reg, registry.AuthConfig{}, err
}

if cfg, ok := getRegistryAuth(reg, cfgs); ok {
return forDockerImage(ctx, img, configs)
}

// forDockerImage returns the auth config for the given Docker image.
func forDockerImage(ctx context.Context, img string, configs map[string]registry.AuthConfig) (string, registry.AuthConfig, error) {
defaultRegistry := defaultRegistryFn(ctx)
reg := core.ExtractRegistry(img, defaultRegistry)

if cfg, ok := getRegistryAuth(reg, configs); ok {
return reg, cfg, nil
}

Expand All @@ -54,24 +65,93 @@ func defaultRegistry(ctx context.Context) string {
return info.IndexServerAddress
}

// authConfig represents the details of the auth config for a registry.
type authConfig struct {
// 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{}}

// Get returns the username and password for the given hostname
// as determined by the details in configPath.
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 := dockercfg.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
}

// configFileKey returns a key to use for caching credentials based on
// the contents of the currently active config.
func configFileKey() (string, error) {
configPath, err := dockercfg.ConfigPath()
if err != nil {
return "", err
}

f, err := os.Open(configPath)
if err != nil {
return "", fmt.Errorf("open config file: %w", err)
}

defer f.Close()

h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("copying config file: %w", err)
}

return hex.EncodeToString(h.Sum(nil)), nil
}

// getDockerAuthConfigs returns a map with the auth configs from the docker config file
// GetDockerConfigs returns a map with the auth configs from the docker config file
// using the registry as the key
var getDockerAuthConfigs = sync.OnceValues(func() (map[string]registry.AuthConfig, error) {
func GetDockerConfigs() (map[string]registry.AuthConfig, error) {
cfg, err := getDockerConfig()
if err != nil {
return nil, err
}

cfgs := map[string]registry.AuthConfig{}
results := make(chan authConfig, len(cfg.AuthConfigs)+len(cfg.CredentialHelpers))
configKey, err := configFileKey()
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(len(cfg.AuthConfigs) + len(cfg.CredentialHelpers))
wg.Add(size)
for k, v := range cfg.AuthConfigs {
go func(k string, v dockercfg.AuthConfig) {
defer wg.Done()
Expand All @@ -86,16 +166,23 @@ var getDockerAuthConfigs = sync.OnceValues(func() (map[string]registry.AuthConfi
Username: v.Username,
}

if v.Username == "" && v.Password == "" {
u, p, _ := dockercfg.GetRegistryCredentials(k)
switch {
case ac.Username == "" && ac.Password == "":
// Look up credentials from the credential store.
u, p, err := creds.Get(k, configKey)
if err != nil {
results <- authConfigResult{err: err}
return
}

ac.Username = u
ac.Password = p
}

if v.Auth == "" {
case ac.Auth == "":
// Create auth from the username and password encoding.
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
}
results <- authConfig{key: k, cfg: ac}

results <- authConfigResult{key: k, cfg: ac}
}(k, v)
}

Expand All @@ -105,11 +192,19 @@ var getDockerAuthConfigs = sync.OnceValues(func() (map[string]registry.AuthConfi
go func(k string) {
defer wg.Done()

ac := registry.AuthConfig{}
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
results <- authConfig{key: k, cfg: ac}
u, p, err := creds.Get(k, configKey)
if err != nil {
results <- authConfigResult{err: err}
return
}

results <- authConfigResult{
key: k,
cfg: registry.AuthConfig{
Username: u,
Password: p,
},
}
}(k)
}

Expand All @@ -118,12 +213,22 @@ var getDockerAuthConfigs = sync.OnceValues(func() (map[string]registry.AuthConfi
close(results)
}()

for ac := range results {
cfgs[ac.key] = ac.cfg
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
Expand Down
31 changes: 31 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package auth

import (
"context"
_ "embed"
"path/filepath"
"testing"

"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -181,3 +183,32 @@ func TestGetDockerConfig(t *testing.T) {
assert.Equal(t, imageReg, registry)
})
}

//go:embed testdata/.docker/config.json
var dockerConfig string

func TestGetDockerConfigs(t *testing.T) {
t.Run("file", func(t *testing.T) {
got, err := GetDockerConfigs()
require.NoError(t, err)
require.NotNil(t, got)
})

t.Run("env", func(t *testing.T) {
t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig)

got, err := GetDockerConfigs()
require.NoError(t, err)

// We can only check the keys as the values are not deterministic.
expected := map[string]registry.AuthConfig{
"https://index.docker.io/v1/": {},
"https://example.com": {},
"https://my.private.registry": {},
}
for k := range got {
got[k] = registry.AuthConfig{}
}
require.Equal(t, expected, got)
})
}
1 change: 1 addition & 0 deletions commons-test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test-%: $(GOBIN)/gotestsum
--packages="./..." \
--junitfile TEST-unit.xml \
-- \
-v \
-coverprofile=coverage.out \
-timeout=30m

Expand Down
2 changes: 1 addition & 1 deletion container.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func Run(ctx context.Context, req Request) (*DockerContainer, error) {

if req.Started && !c.IsRunning() {
if err := c.Start(ctx); err != nil {
return c, fmt.Errorf("failed to start container: %w", err)
return c, fmt.Errorf("start container: %w", err)
}
}
return c, nil
Expand Down
15 changes: 7 additions & 8 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func TestBuildImageWithContexts(t *testing.T) {
type TestCase struct {
Name string
ContextPath string
ContextArchive func() (io.Reader, error)
ContextArchive func() (io.ReadSeeker, error)
ExpectedEchoOutput string
Dockerfile string
ExpectedError string
Expand All @@ -157,7 +157,7 @@ func TestBuildImageWithContexts(t *testing.T) {
{
Name: "test build from context archive",
// fromDockerfileWithContextArchive {
ContextArchive: func() (io.Reader, error) {
ContextArchive: func() (io.ReadSeeker, error) {
var buf bytes.Buffer
tarWriter := tar.NewWriter(&buf)
files := []struct {
Expand Down Expand Up @@ -202,7 +202,7 @@ func TestBuildImageWithContexts(t *testing.T) {
},
{
Name: "test build from context archive and be able to use files in it",
ContextArchive: func() (io.Reader, error) {
ContextArchive: func() (io.ReadSeeker, error) {
var buf bytes.Buffer
tarWriter := tar.NewWriter(&buf)
files := []struct {
Expand Down Expand Up @@ -255,14 +255,14 @@ func TestBuildImageWithContexts(t *testing.T) {
ContextPath: "./testdata",
Dockerfile: "echo.Dockerfile",
ExpectedEchoOutput: "this is from the echo test Dockerfile",
ContextArchive: func() (io.Reader, error) {
ContextArchive: func() (io.ReadSeeker, error) {
return nil, nil
},
},
{
Name: "it should error if neither a context nor a context archive are specified",
ContextPath: "",
ContextArchive: func() (io.Reader, error) {
ContextArchive: func() (io.ReadSeeker, error) {
return nil, nil
},
ExpectedError: "create container: you must specify either a build context or an image",
Expand All @@ -275,9 +275,8 @@ func TestBuildImageWithContexts(t *testing.T) {
t.Parallel()
ctx := context.Background()
a, err := testCase.ContextArchive()
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

req := testcontainers.Request{
FromDockerfile: testcontainers.FromDockerfile{
ContextArchive: a,
Expand Down
6 changes: 3 additions & 3 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,12 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tce

response, err := cli.ContainerExecCreate(ctx, c.ID, processOptions.ExecConfig)
if err != nil {
return 0, nil, err
return 0, nil, fmt.Errorf("container exec create: %w", err)
}

hijack, err := cli.ContainerExecAttach(ctx, response.ID, container.ExecAttachOptions{})
if err != nil {
return 0, nil, err
return 0, nil, fmt.Errorf("container exec attach: %w", err)
}

processOptions.Reader = hijack.Reader
Expand All @@ -333,7 +333,7 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tce
for {
execResp, err := cli.ContainerExecInspect(ctx, response.ID)
if err != nil {
return 0, nil, err
return 0, nil, fmt.Errorf("container exec inspect: %w", err)
}

if !execResp.Running {
Expand Down
Loading

0 comments on commit 6e6c62c

Please sign in to comment.