Skip to content

Commit

Permalink
feat: support for reading auth credentials from docker credential hel…
Browse files Browse the repository at this point in the history
…pers (#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
  • Loading branch information
mdelapenya authored Mar 7, 2023
1 parent e67432e commit b56e66a
Show file tree
Hide file tree
Showing 20 changed files with 865 additions and 218 deletions.
25 changes: 21 additions & 4 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/docker/go-connections/nat"

tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/wait"
)

Expand Down Expand Up @@ -84,7 +85,7 @@ type FromDockerfile struct {
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
BuildArgs map[string]*string // enable user to pass build args to docker daemon
PrintBuildLog bool // enable user to print build log
AuthConfigs map[string]types.AuthConfig // enable auth configs to be able to pull from an authenticated docker registry
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
}

type ContainerFile struct {
Expand All @@ -104,7 +105,7 @@ type ContainerRequest struct {
Labels map[string]string
Mounts ContainerMounts
Tmpfs map[string]string
RegistryCred string
RegistryCred string // Deprecated: Testcontainers will detect registry credentials automatically
WaitingFor wait.Strategy
Name string // for specifying container name
Hostname string
Expand Down Expand Up @@ -158,7 +159,7 @@ func (f GenericProviderOptionFunc) ApplyGenericTo(opts *GenericProviderOptions)
// containerOptions functional options for a container
type containerOptions struct {
ImageName string
RegistryCredentials string
RegistryCredentials string // Deprecated: Testcontainers will detect registry credentials automatically
}

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

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

// GetAuthConfigs returns the auth configs to be able to pull from an authenticated docker registry
func (c *ContainerRequest) GetAuthConfigs() map[string]types.AuthConfig {
return c.FromDockerfile.AuthConfigs
images, err := testcontainersdocker.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
if err != nil {
return map[string]types.AuthConfig{}
}

authConfigs := map[string]types.AuthConfig{}
for _, image := range images {
registry, authConfig, err := DockerImageAuth(context.Background(), image)
if err != nil {
continue
}

authConfigs[registry] = authConfig
}

return authConfigs
}

func (c *ContainerRequest) ShouldBuildImage() bool {
Expand Down
45 changes: 0 additions & 45 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"testing"
"time"

"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"

"github.com/testcontainers/testcontainers-go/wait"
Expand Down Expand Up @@ -127,50 +126,6 @@ func Test_GetDockerfile(t *testing.T) {
}
}

func Test_GetAuthConfigs(t *testing.T) {
type TestCase struct {
name string
ExpectedAuthConfigs map[string]types.AuthConfig
ContainerRequest ContainerRequest
}

testTable := []TestCase{
{
name: "defaults to no auth",
ExpectedAuthConfigs: nil,
ContainerRequest: ContainerRequest{
FromDockerfile: FromDockerfile{},
},
},
{
name: "will specify credentials",
ExpectedAuthConfigs: map[string]types.AuthConfig{
"https://myregistry.com/": {
Username: "username",
Password: "password",
},
},
ContainerRequest: ContainerRequest{
FromDockerfile: FromDockerfile{
AuthConfigs: map[string]types.AuthConfig{
"https://myregistry.com/": {
Username: "username",
Password: "password",
},
},
},
},
},
}

for _, testCase := range testTable {
t.Run(testCase.name, func(t *testing.T) {
cfgs := testCase.ContainerRequest.GetAuthConfigs()
assert.Equal(t, testCase.ExpectedAuthConfigs, cfgs)
})
}
}

func Test_BuildImageWithContexts(t *testing.T) {
type TestCase struct {
Name string
Expand Down
15 changes: 13 additions & 2 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"bufio"
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -1046,8 +1048,17 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
Platform: req.ImagePlatform, // may be empty
}

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

if err := p.attemptToPullImage(ctx, tag, pullOpt); err != nil {
Expand Down
107 changes: 107 additions & 0 deletions docker_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package testcontainers

import (
"context"
"encoding/base64"
"encoding/json"
"os"

"github.com/cpuguy83/dockercfg"
"github.com/docker/docker/api/types"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
)

// 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, types.AuthConfig, error) {
defaultRegistry := defaultRegistry(ctx)
registry := testcontainersdocker.ExtractRegistry(image, defaultRegistry)

cfgs, err := getDockerAuthConfigs()
if err != nil {
return registry, types.AuthConfig{}, err
}

if cfg, ok := cfgs[registry]; ok {
return registry, cfg, nil
}

return registry, types.AuthConfig{}, dockercfg.ErrCredentialsNotFound
}

// 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 {
p, err := NewDockerProvider()
if err != nil {
return testcontainersdocker.IndexDockerIO
}

info, err := p.client.Info(ctx)
if err != nil {
return testcontainersdocker.IndexDockerIO
}

return info.IndexServerAddress
}

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

cfgs := map[string]types.AuthConfig{}
for k, v := range cfg.AuthConfigs {
ac := types.AuthConfig{
Auth: v.Auth,
Email: v.Email,
IdentityToken: v.IdentityToken,
Password: v.Password,
RegistryToken: v.RegistryToken,
ServerAddress: v.ServerAddress,
Username: v.Username,
}

if v.Username == "" && v.Password == "" {
u, p, _ := dockercfg.GetRegistryCredentials(k)
ac.Username = u
ac.Password = p
}

if v.Auth == "" {
ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password))
}

cfgs[k] = ac
}

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) {
dockerAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG")
if dockerAuthConfig != "" {
cfg := dockercfg.Config{}
err := json.Unmarshal([]byte(dockerAuthConfig), &cfg)
if err == nil {
return cfg, nil
}

}

cfg, err := dockercfg.LoadDefaultConfig()
if err != nil {
return cfg, err
}

return cfg, nil
}
Loading

0 comments on commit b56e66a

Please sign in to comment.