Skip to content

Commit b56e66a

Browse files
authored
feat: support for reading auth credentials from docker credential helpers (#869)
* feat: expose a way to retrieve credentials from the credentials helpers * chore: support for retrieving the config file from the DOCKER_CONFIG env var * docs: document how to retrieve Docker credentials * chore: support reading from DOCKER_AUTH_CONFIG * feat: populate the auth struct from the Docker credentials helper if user and password are empty in the configuration file * chore: simplify * chore: load from credentials helper properly * docs: typo * chore: move to constants * chor: do not expose helper method * chore: support getting auth for default registry * chore: friendlier func names * chore: extract registry from a Docker image * chore: move constant to internal package * chore: return default Docker registry if no registry is found * chore: define a fallback * chore: include protocol of the registry * chore: refactor func to get the auth from a Docker image * chore: do not hardcode ports in tests * fix: apply credentials to the right struct * feart: pull image using the registry credentials of the image * feat: support extracting all images from a Dockerfile * chore: deprecated AuthConfigs from BuildFormDockerfile They will automatically discovered extracting all the FROM images in the Dockerfiles * Revert "chore: do not hardcode ports in tests" This reverts commit 24ac700. * chore: do not recalculate base64 if it exists * chore: set custom Auth config for the registry tests * chore: remove deprecated AuthConfigs usage * chore: set proper docker config for tests involving a private registry * chore: avoid double call to extract registry * chore: extract expected registries to constants * fix: use the right format for the registry auth * chore: improve test names * chore: better test names * chore: move docker auth tests to another test file * docs: update docs * chore: interpolate build args when extracting images from dockerfile * chore: remove logs from tests * chore: extract message to constant * fix: remove whitespaces from each line * chore: verify that Docker's default config file exist
1 parent e67432e commit b56e66a

20 files changed

+865
-218
lines changed

container.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/docker/go-connections/nat"
1616

1717
tcexec "github.com/testcontainers/testcontainers-go/exec"
18+
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
1819
"github.com/testcontainers/testcontainers-go/wait"
1920
)
2021

@@ -84,7 +85,7 @@ type FromDockerfile struct {
8485
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
8586
BuildArgs map[string]*string // enable user to pass build args to docker daemon
8687
PrintBuildLog bool // enable user to print build log
87-
AuthConfigs map[string]types.AuthConfig // enable auth configs to be able to pull from an authenticated docker registry
88+
AuthConfigs map[string]types.AuthConfig // Deprecated. Testcontainers will detect registry credentials automatically. Enable auth configs to be able to pull from an authenticated docker registry
8889
}
8990

9091
type ContainerFile struct {
@@ -104,7 +105,7 @@ type ContainerRequest struct {
104105
Labels map[string]string
105106
Mounts ContainerMounts
106107
Tmpfs map[string]string
107-
RegistryCred string
108+
RegistryCred string // Deprecated: Testcontainers will detect registry credentials automatically
108109
WaitingFor wait.Strategy
109110
Name string // for specifying container name
110111
Hostname string
@@ -158,7 +159,7 @@ func (f GenericProviderOptionFunc) ApplyGenericTo(opts *GenericProviderOptions)
158159
// containerOptions functional options for a container
159160
type containerOptions struct {
160161
ImageName string
161-
RegistryCredentials string
162+
RegistryCredentials string // Deprecated: Testcontainers will detect registry credentials automatically
162163
}
163164

164165
// functional option for setting the reaper image
@@ -171,6 +172,7 @@ func WithImageName(imageName string) ContainerOption {
171172
}
172173
}
173174

175+
// Deprecated: Testcontainers will detect registry credentials automatically
174176
// WithRegistryCredentials sets the reaper registry credentials
175177
func WithRegistryCredentials(registryCredentials string) ContainerOption {
176178
return func(o *containerOptions) {
@@ -271,7 +273,22 @@ func (c *ContainerRequest) GetDockerfile() string {
271273

272274
// GetAuthConfigs returns the auth configs to be able to pull from an authenticated docker registry
273275
func (c *ContainerRequest) GetAuthConfigs() map[string]types.AuthConfig {
274-
return c.FromDockerfile.AuthConfigs
276+
images, err := testcontainersdocker.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
277+
if err != nil {
278+
return map[string]types.AuthConfig{}
279+
}
280+
281+
authConfigs := map[string]types.AuthConfig{}
282+
for _, image := range images {
283+
registry, authConfig, err := DockerImageAuth(context.Background(), image)
284+
if err != nil {
285+
continue
286+
}
287+
288+
authConfigs[registry] = authConfig
289+
}
290+
291+
return authConfigs
275292
}
276293

277294
func (c *ContainerRequest) ShouldBuildImage() bool {

container_test.go

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"testing"
1212
"time"
1313

14-
"github.com/docker/docker/api/types"
1514
"github.com/stretchr/testify/assert"
1615

1716
"github.com/testcontainers/testcontainers-go/wait"
@@ -127,50 +126,6 @@ func Test_GetDockerfile(t *testing.T) {
127126
}
128127
}
129128

130-
func Test_GetAuthConfigs(t *testing.T) {
131-
type TestCase struct {
132-
name string
133-
ExpectedAuthConfigs map[string]types.AuthConfig
134-
ContainerRequest ContainerRequest
135-
}
136-
137-
testTable := []TestCase{
138-
{
139-
name: "defaults to no auth",
140-
ExpectedAuthConfigs: nil,
141-
ContainerRequest: ContainerRequest{
142-
FromDockerfile: FromDockerfile{},
143-
},
144-
},
145-
{
146-
name: "will specify credentials",
147-
ExpectedAuthConfigs: map[string]types.AuthConfig{
148-
"https://myregistry.com/": {
149-
Username: "username",
150-
Password: "password",
151-
},
152-
},
153-
ContainerRequest: ContainerRequest{
154-
FromDockerfile: FromDockerfile{
155-
AuthConfigs: map[string]types.AuthConfig{
156-
"https://myregistry.com/": {
157-
Username: "username",
158-
Password: "password",
159-
},
160-
},
161-
},
162-
},
163-
},
164-
}
165-
166-
for _, testCase := range testTable {
167-
t.Run(testCase.name, func(t *testing.T) {
168-
cfgs := testCase.ContainerRequest.GetAuthConfigs()
169-
assert.Equal(t, testCase.ExpectedAuthConfigs, cfgs)
170-
})
171-
}
172-
}
173-
174129
func Test_BuildImageWithContexts(t *testing.T) {
175130
type TestCase struct {
176131
Name string

docker.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"bufio"
66
"bytes"
77
"context"
8+
"encoding/base64"
89
"encoding/binary"
10+
"encoding/json"
911
"errors"
1012
"fmt"
1113
"io"
@@ -1046,8 +1048,17 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
10461048
Platform: req.ImagePlatform, // may be empty
10471049
}
10481050

1049-
if req.RegistryCred != "" {
1050-
pullOpt.RegistryAuth = req.RegistryCred
1051+
registry, imageAuth, err := DockerImageAuth(ctx, req.Image)
1052+
if err != nil {
1053+
p.Logger.Printf("Failed to get image auth for %s. Setting empty credentials for the image: %s. Error is:%s", registry, req.Image, err)
1054+
} else {
1055+
// see https://github.com/docker/docs/blob/e8e1204f914767128814dca0ea008644709c117f/engine/api/sdk/examples.md?plain=1#L649-L657
1056+
encodedJSON, err := json.Marshal(imageAuth)
1057+
if err != nil {
1058+
p.Logger.Printf("Failed to marshal image auth. Setting empty credentials for the image: %s. Error is:%s", req.Image, err)
1059+
} else {
1060+
pullOpt.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
1061+
}
10511062
}
10521063

10531064
if err := p.attemptToPullImage(ctx, tag, pullOpt); err != nil {

docker_auth.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package testcontainers
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"os"
8+
9+
"github.com/cpuguy83/dockercfg"
10+
"github.com/docker/docker/api/types"
11+
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
12+
)
13+
14+
// DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
15+
// Finally, it will use the credential helpers to extract the information from the docker config file
16+
// for that registry, if it exists.
17+
func DockerImageAuth(ctx context.Context, image string) (string, types.AuthConfig, error) {
18+
defaultRegistry := defaultRegistry(ctx)
19+
registry := testcontainersdocker.ExtractRegistry(image, defaultRegistry)
20+
21+
cfgs, err := getDockerAuthConfigs()
22+
if err != nil {
23+
return registry, types.AuthConfig{}, err
24+
}
25+
26+
if cfg, ok := cfgs[registry]; ok {
27+
return registry, cfg, nil
28+
}
29+
30+
return registry, types.AuthConfig{}, dockercfg.ErrCredentialsNotFound
31+
}
32+
33+
// defaultRegistry returns the default registry to use when pulling images
34+
// It will use the docker daemon to get the default registry, returning "https://index.docker.io/v1/" if
35+
// it fails to get the information from the daemon
36+
func defaultRegistry(ctx context.Context) string {
37+
p, err := NewDockerProvider()
38+
if err != nil {
39+
return testcontainersdocker.IndexDockerIO
40+
}
41+
42+
info, err := p.client.Info(ctx)
43+
if err != nil {
44+
return testcontainersdocker.IndexDockerIO
45+
}
46+
47+
return info.IndexServerAddress
48+
}
49+
50+
// getDockerAuthConfigs returns a map with the auth configs from the docker config file
51+
// using the registry as the key
52+
func getDockerAuthConfigs() (map[string]types.AuthConfig, error) {
53+
cfg, err := getDockerConfig()
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
cfgs := map[string]types.AuthConfig{}
59+
for k, v := range cfg.AuthConfigs {
60+
ac := types.AuthConfig{
61+
Auth: v.Auth,
62+
Email: v.Email,
63+
IdentityToken: v.IdentityToken,
64+
Password: v.Password,
65+
RegistryToken: v.RegistryToken,
66+
ServerAddress: v.ServerAddress,
67+
Username: v.Username,
68+
}
69+
70+
if v.Username == "" && v.Password == "" {
71+
u, p, _ := dockercfg.GetRegistryCredentials(k)
72+
ac.Username = u
73+
ac.Password = p
74+
}
75+
76+
if v.Auth == "" {
77+
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
78+
}
79+
80+
cfgs[k] = ac
81+
}
82+
83+
return cfgs, nil
84+
}
85+
86+
// getDockerConfig returns the docker config file. It will internally check, in this particular order:
87+
// 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a dockercfg.Config
88+
// 2. the DOCKER_CONFIG environment variable, as the path to the config file
89+
// 3. else it will load the default config file, which is ~/.docker/config.json
90+
func getDockerConfig() (dockercfg.Config, error) {
91+
dockerAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG")
92+
if dockerAuthConfig != "" {
93+
cfg := dockercfg.Config{}
94+
err := json.Unmarshal([]byte(dockerAuthConfig), &cfg)
95+
if err == nil {
96+
return cfg, nil
97+
}
98+
99+
}
100+
101+
cfg, err := dockercfg.LoadDefaultConfig()
102+
if err != nil {
103+
return cfg, err
104+
}
105+
106+
return cfg, nil
107+
}

0 commit comments

Comments
 (0)