Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: docker authentication setup #2727

Merged
merged 1 commit into from
Aug 16, 2024
Merged
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
90 changes: 78 additions & 12 deletions container.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package testcontainers

import (
"archive/tar"
"context"
"errors"
"fmt"
Expand All @@ -10,6 +11,7 @@ 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 Down Expand Up @@ -85,7 +87,7 @@ type ImageBuildInfo interface {
// rather than using a pre-built one
type FromDockerfile struct {
Context string // the path to the context of the docker build
ContextArchive io.Reader // the tar archive file to send to docker that contains the build context
ContextArchive io.ReadSeeker // the tar archive file to send to docker that contains the build context
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
Dockerfile string // the path from the context to the Dockerfile for the image, defaults to "Dockerfile"
Repo string // the repo label for image, defaults to UUID
Tag string // the tag label for image, defaults to UUID
Expand Down Expand Up @@ -305,30 +307,90 @@ func (c *ContainerRequest) GetTag() string {
return strings.ToLower(t)
}

// Deprecated: Testcontainers will detect registry credentials automatically, and it will be removed in the next major release
// GetAuthConfigs returns the auth configs to be able to pull from an authenticated docker registry
// Deprecated: Testcontainers will detect registry credentials automatically, and it will be removed in the next major release.
// GetAuthConfigs returns the auth configs to be able to pull from an authenticated docker registry.
// Panics if an error occurs.
func (c *ContainerRequest) GetAuthConfigs() map[string]registry.AuthConfig {
return getAuthConfigsFromDockerfile(c)
auth, err := getAuthConfigsFromDockerfile(c)
if err != nil {
panic(fmt.Sprintf("failed to get auth configs from Dockerfile: %v", err))
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
}
return auth
}

// dockerFileImages returns the images from the request Dockerfile.
func (c *ContainerRequest) dockerFileImages() ([]string, error) {
if c.ContextArchive == nil {
// Source is a directory, we can read the Dockerfile directly.
images, err := core.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
if err != nil {
return nil, fmt.Errorf("extract images from Dockerfile: %w", err)
}

return images, nil
}

// Source is an archive, we need to read it to get the Dockerfile.
dockerFile := c.GetDockerfile()
tr := tar.NewReader(c.FromDockerfile.ContextArchive)

for {
hdr, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
return nil, fmt.Errorf("Dockerfile %q not found in context archive", dockerFile)
}

return nil, fmt.Errorf("reading tar archive: %w", err)
}

if hdr.Name != dockerFile {
continue
}

images, err := core.ExtractImagesFromReader(tr, c.GetBuildArgs())
if err != nil {
return nil, fmt.Errorf("extract images from Dockerfile: %w", err)
}

// Reset the archive to the beginning.
if _, err := c.ContextArchive.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("seek context archive to start: %w", err)
}

return images, nil
}
}

// getAuthConfigsFromDockerfile returns the auth configs to be able to pull from an authenticated docker registry
func getAuthConfigsFromDockerfile(c *ContainerRequest) map[string]registry.AuthConfig {
images, err := core.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
func getAuthConfigsFromDockerfile(c *ContainerRequest) (map[string]registry.AuthConfig, error) {
images, err := c.dockerFileImages()
if err != nil {
return map[string]registry.AuthConfig{}
return nil, fmt.Errorf("docker file images: %w", err)
}

// Get the auth configs once for all images as it can be a time-consuming operation.
configs, err := getDockerAuthConfigs()
if err != nil {
return nil, err
}

authConfigs := map[string]registry.AuthConfig{}
for _, image := range images {
registry, authConfig, err := DockerImageAuth(context.Background(), image)
registry, authConfig, err := dockerImageAuth(context.Background(), image, configs)
stevenh marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
if !errors.Is(err, dockercfg.ErrCredentialsNotFound) {
return nil, fmt.Errorf("docker image auth %q: %w", image, err)
}

// Credentials not found no config to add.
continue
}

authConfigs[registry] = authConfig
}

return authConfigs
return authConfigs, nil
}

func (c *ContainerRequest) ShouldBuildImage() bool {
Expand Down Expand Up @@ -361,7 +423,10 @@ func (c *ContainerRequest) BuildOptions() (types.ImageBuildOptions, error) {
buildOptions.Dockerfile = c.GetDockerfile()

// Make sure the auth configs from the Dockerfile are set right after the user-defined build options.
authsFromDockerfile := getAuthConfigsFromDockerfile(c)
authsFromDockerfile, err := getAuthConfigsFromDockerfile(c)
if err != nil {
return types.ImageBuildOptions{}, fmt.Errorf("auth configs from Dockerfile: %w", err)
}

if buildOptions.AuthConfigs == nil {
buildOptions.AuthConfigs = map[string]registry.AuthConfig{}
Expand All @@ -378,7 +443,7 @@ func (c *ContainerRequest) BuildOptions() (types.ImageBuildOptions, error) {
for _, is := range c.ImageSubstitutors {
modifiedTag, err := is.Substitute(tag)
if err != nil {
return buildOptions, fmt.Errorf("failed to substitute image %s with %s: %w", tag, is.Description(), err)
return types.ImageBuildOptions{}, fmt.Errorf("failed to substitute image %s with %s: %w", tag, is.Description(), err)
stevenh marked this conversation as resolved.
Show resolved Hide resolved
}

if modifiedTag != tag {
Expand All @@ -401,8 +466,9 @@ func (c *ContainerRequest) BuildOptions() (types.ImageBuildOptions, error) {
// Do this as late as possible to ensure we don't leak the context on error/panic.
buildContext, err := c.GetContext()
if err != nil {
return buildOptions, err
return types.ImageBuildOptions{}, err
}

buildOptions.Context = buildContext

return buildOptions, 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 Test_BuildImageWithContexts(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 Test_BuildImageWithContexts(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 Test_BuildImageWithContexts(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 Test_BuildImageWithContexts(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 Test_BuildImageWithContexts(t *testing.T) {
t.Parallel()
ctx := context.Background()
a, err := testCase.ContextArchive()
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)

req := testcontainers.ContainerRequest{
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 @@ -1114,7 +1114,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
// forward the host ports to the container ports.
sshdForwardPortsHook, err := exposeHostPorts(ctx, &req, req.HostAccessPorts...)
if err != nil {
return nil, fmt.Errorf("failed to expose host ports: %w", err)
return nil, fmt.Errorf("expose host ports: %w", err)
}

defaultHooks = append(defaultHooks, sshdForwardPortsHook)
Expand Down Expand Up @@ -1292,12 +1292,12 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pullOpt image.PullOptions) error {
registry, imageAuth, err := DockerImageAuth(ctx, tag)
if err != nil {
p.Logger.Printf("Failed to get image auth for %s. Setting empty credentials for the image: %s. Error is:%s", registry, tag, err)
p.Logger.Printf("Failed to get image auth for %s. Setting empty credentials for the image: %s. Error is: %s", registry, tag, 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", tag, err)
p.Logger.Printf("Failed to marshal image auth. Setting empty credentials for the image: %s. Error is: %s", tag, err)
} else {
pullOpt.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
}
Expand Down
Loading