Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"strings"
"time"

"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
Expand All @@ -23,6 +22,7 @@ import (

tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/core"
dockerconfig "github.com/testcontainers/testcontainers-go/internal/docker/config"
"github.com/testcontainers/testcontainers-go/log"
"github.com/testcontainers/testcontainers-go/wait"
)
Expand Down Expand Up @@ -390,7 +390,7 @@ func getAuthConfigsFromDockerfile(c *ContainerRequest) (map[string]registry.Auth
for _, image := range images {
registry, authConfig, err := dockerImageAuth(context.Background(), image, configs)
if err != nil {
if !errors.Is(err, dockercfg.ErrCredentialsNotFound) {
if !errors.Is(err, dockerconfig.ErrCredentialsNotFound) {
return nil, fmt.Errorf("docker image auth %q: %w", image, err)
}

Expand Down
38 changes: 8 additions & 30 deletions docker_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import (
"os"
"sync"

"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types/registry"

"github.com/testcontainers/testcontainers-go/internal/core"
dockerconfig "github.com/testcontainers/testcontainers-go/internal/docker/config"
)

// 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
// getRegistryCredentials is a variable overwritten in tests to mock the dockerconfig.GetRegistryCredentials function.
var getRegistryCredentials = dockerconfig.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
Expand All @@ -46,7 +46,7 @@ func dockerImageAuth(ctx context.Context, image string, configs map[string]regis
return reg, cfg, nil
}

return reg, registry.AuthConfig{}, dockercfg.ErrCredentialsNotFound
return reg, registry.AuthConfig{}, dockerconfig.ErrCredentialsNotFound
}

func getRegistryAuth(reg string, cfgs map[string]registry.AuthConfig) (registry.AuthConfig, bool) {
Expand Down Expand Up @@ -160,7 +160,7 @@ func (c *credentialsCache) get(hostname, configKey string) (string, string, erro

// 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) {
func configKey(cfg *dockerconfig.Config) (string, error) {
h := md5.New()
if err := json.NewEncoder(h).Encode(cfg); err != nil {
return "", fmt.Errorf("encode config: %w", err)
Expand All @@ -172,7 +172,7 @@ func configKey(cfg *dockercfg.Config) (string, error) {
// 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()
cfg, err := dockerconfig.Load()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return map[string]registry.AuthConfig{}, nil
Expand All @@ -181,7 +181,7 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
return nil, err
}

key, err := configKey(cfg)
key, err := configKey(&cfg)
if err != nil {
return nil, err
}
Expand All @@ -192,7 +192,7 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {
var wg sync.WaitGroup
wg.Add(size)
for k, v := range cfg.AuthConfigs {
go func(k string, v dockercfg.AuthConfig) {
go func(k string, v dockerconfig.AuthConfig) {
defer wg.Done()

ac := registry.AuthConfig{
Expand Down Expand Up @@ -258,25 +258,3 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) {

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
}
221 changes: 4 additions & 217 deletions docker_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,95 +7,20 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"testing"

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

"github.com/testcontainers/testcontainers-go/internal/core"
dockerconfig "github.com/testcontainers/testcontainers-go/internal/docker/config"
"github.com/testcontainers/testcontainers-go/wait"
)

const (
exampleAuth = "https://example-auth.com"
privateRegistry = "https://my.private.registry"
exampleRegistry = "https://example.com"
exampleAuth = "https://example-auth.com"
)

func Test_getDockerConfig(t *testing.T) {
expectedConfig := &dockercfg.Config{
AuthConfigs: map[string]dockercfg.AuthConfig{
core.IndexDockerIO: {},
exampleRegistry: {},
privateRegistry: {},
},
CredentialsStore: "desktop",
}
t.Run("HOME/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata")

cfg, err := getDockerConfig()
require.NoError(t, err)
require.Equal(t, expectedConfig, cfg)
})

t.Run("HOME/not-found", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")

cfg, err := getDockerConfig()
require.ErrorIs(t, err, os.ErrNotExist)
require.Nil(t, cfg)
})

t.Run("HOME/invalid-config", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "invalid-config")

cfg, err := getDockerConfig()
require.ErrorContains(t, err, "json: cannot unmarshal array")
require.Nil(t, cfg)
})

t.Run("DOCKER_AUTH_CONFIG/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig)

cfg, err := getDockerConfig()
require.NoError(t, err)
require.Equal(t, expectedConfig, cfg)
})

t.Run("DOCKER_AUTH_CONFIG/invalid-config", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_AUTH_CONFIG", `{"auths": []}`)

cfg, err := getDockerConfig()
require.ErrorContains(t, err, "json: cannot unmarshal array")
require.Nil(t, cfg)
})

t.Run("DOCKER_CONFIG/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", ".docker"))

cfg, err := getDockerConfig()
require.NoError(t, err)
require.Equal(t, expectedConfig, cfg)
})

t.Run("DOCKER_CONFIG/invalid-config", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", "invalid-config", ".docker"))

cfg, err := getDockerConfig()
require.ErrorContains(t, err, "json: cannot unmarshal array")
require.Nil(t, cfg)
})
}

func TestDockerImageAuth(t *testing.T) {
t.Run("retrieve auth with DOCKER_AUTH_CONFIG env var", func(t *testing.T) {
username, password := "gopher", "secret"
Expand Down Expand Up @@ -130,7 +55,7 @@ func TestDockerImageAuth(t *testing.T) {
setAuthConfig(t, invalidRegistryURL, "gopher", "secret")

registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath)
require.ErrorIs(t, err, dockercfg.ErrCredentialsNotFound)
require.ErrorIs(t, err, dockerconfig.ErrCredentialsNotFound)
require.Empty(t, cfg)
require.Equal(t, imageReg, registry)
})
Expand All @@ -150,7 +75,7 @@ func TestDockerImageAuth(t *testing.T) {
setAuthConfig(t, "example-auth.com", "gopher", "secret")

registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath)
require.ErrorIs(t, err, dockercfg.ErrCredentialsNotFound)
require.ErrorIs(t, err, dockerconfig.ErrCredentialsNotFound)
require.Empty(t, cfg)
require.Equal(t, imageReg, registry)
})
Expand Down Expand Up @@ -378,141 +303,3 @@ func localAddress(t *testing.T) string {

return localAddr.IP.String()
}

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

// reset resets the credentials cache.
func (c *credentialsCache) reset() {
c.mtx.Lock()
defer c.mtx.Unlock()
c.entries = make(map[string]credentials)
}

func Test_getDockerAuthConfigs(t *testing.T) {
t.Run("HOME/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata")

requireValidAuthConfig(t)
})

t.Run("HOME/not-found", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-exist")

authConfigs, err := getDockerAuthConfigs()
require.NoError(t, err)
require.NotNil(t, authConfigs)
require.Empty(t, authConfigs)
})

t.Run("HOME/invalid-config", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "invalid-config")

authConfigs, err := getDockerAuthConfigs()
require.ErrorContains(t, err, "json: cannot unmarshal array")
require.Nil(t, authConfigs)
})

t.Run("DOCKER_AUTH_CONFIG/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-exist")
t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig)

requireValidAuthConfig(t)
})

t.Run("DOCKER_AUTH_CONFIG/invalid-config", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-exist")
t.Setenv("DOCKER_AUTH_CONFIG", `{"auths": []}`)

authConfigs, err := getDockerAuthConfigs()
require.ErrorContains(t, err, "json: cannot unmarshal array")
require.Nil(t, authConfigs)
})

t.Run("DOCKER_AUTH_CONFIG/identity-token", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-exist")

// Reset the credentials cache to ensure our mocked method is called.
creds.reset()

// Mock getRegistryCredentials to return identity-token for index.docker.io.
old := getRegistryCredentials
t.Cleanup(func() {
getRegistryCredentials = old
creds.reset() // Ensure our mocked results aren't cached.
})
getRegistryCredentials = func(hostname string) (string, string, error) {
switch hostname {
case core.IndexDockerIO:
return "", "identity-token", nil
default:
return "username", "password", nil
}
}
t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig)

authConfigs, err := getDockerAuthConfigs()
require.NoError(t, err)
require.Equal(t, map[string]registry.AuthConfig{
core.IndexDockerIO: {
IdentityToken: "identity-token",
},
privateRegistry: {
Username: "username",
Password: "password",
},
exampleRegistry: {
Username: "username",
Password: "password",
},
}, authConfigs)
})

t.Run("DOCKER_CONFIG/valid", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", ".docker"))

requireValidAuthConfig(t)
})

t.Run("DOCKER_CONFIG/invalid-config", func(t *testing.T) {
testDockerConfigHome(t, "testdata", "not-found")
t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", "invalid-config", ".docker"))

cfg, err := getDockerConfig()
require.ErrorContains(t, err, "json: cannot unmarshal array")
require.Nil(t, cfg)
})
}

// requireValidAuthConfig checks that the given authConfigs map contains the expected keys.
func requireValidAuthConfig(t *testing.T) {
t.Helper()

authConfigs, err := getDockerAuthConfigs()
require.NoError(t, err)

// We can only check the keys as the values are not deterministic as they depend
// on user's environment.
expected := map[string]registry.AuthConfig{
core.IndexDockerIO: {},
exampleRegistry: {},
privateRegistry: {},
}
for k := range authConfigs {
authConfigs[k] = registry.AuthConfig{}
}
require.Equal(t, expected, authConfigs)
}

// testDockerConfigHome sets the user's home directory to the given path
// and unsets the DOCKER_CONFIG and DOCKER_AUTH_CONFIG environment variables.
func testDockerConfigHome(t *testing.T, dirs ...string) {
t.Helper()

dir := filepath.Join(dirs...)
t.Setenv("DOCKER_AUTH_CONFIG", "")
t.Setenv("DOCKER_CONFIG", "")
t.Setenv("HOME", dir)
t.Setenv("USERPROFILE", dir) // Windows
}
3 changes: 0 additions & 3 deletions docs/features/docker_auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ and retrieve the authentication for a given registry. To achieve it, _Testcontai

To understand how the Docker credential helpers work, please refer to the [official documentation](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers).

!!! info
_Testcontainers for Go_ uses [https://github.com/cpuguy83/dockercfg](https://github.com/cpuguy83/dockercfg) to retrieve the authentication from the credential helpers.

_Testcontainers for Go_ will automatically discover the credentials for a given Docker image from the Docker config, as described above. For that, it will extract the Docker registry from the image name, and for that registry will try to locate the authentication in the Docker config, returning an empty string if the registry is not found. As a consequence, all the fields to pass credentials to the container request will be deprecated.

```go
Expand Down
1 change: 0 additions & 1 deletion examples/nginx/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.0.1+incompatible // indirect
Expand Down
Loading
Loading