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

feat: Auth config for build images #602

27 changes: 17 additions & 10 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,23 @@ type Container interface {

// ImageBuildInfo defines what is needed to build an image
type ImageBuildInfo interface {
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the fileitself
ShouldPrintBuildLog() bool // allow build log to be printed to stdout
ShouldBuildImage() bool // return true if the image needs to be built
GetBuildArgs() map[string]*string // return the environment args used to build the from Dockerfile
GetContext() (io.Reader, error) // the path to the build context
GetDockerfile() string // the relative path to the Dockerfile, including the fileitself
ShouldPrintBuildLog() bool // allow build log to be printed to stdout
ShouldBuildImage() bool // return true if the image needs to be built
GetBuildArgs() map[string]*string // return the environment args used to build the from Dockerfile
GetAuthConfigs() map[string]types.AuthConfig // return the auth configs to be able to pull from an authenticated docker registry
}

// FromDockerfile represents the parameters needed to build an image from a Dockerfile
// rather than using a pre-built one
type FromDockerfile struct {
Context string // the path to the context of of the docker build
ContextArchive io.Reader // the tar archive file to send to docker that contains the build context
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
Context string // the path to the context of of the docker build
ContextArchive io.Reader // the tar archive file to send to docker that contains the build context
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
}

type ContainerFile struct {
Expand Down Expand Up @@ -230,6 +232,11 @@ func (c *ContainerRequest) GetDockerfile() string {
return f
}

// 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
}

func (c *ContainerRequest) ShouldBuildImage() bool {
return c.FromDockerfile.Context != "" || c.FromDockerfile.ContextArchive != nil
}
Expand Down
45 changes: 45 additions & 0 deletions container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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,6 +128,50 @@ func Test_GetDockerfile(t *testing.T) {
}
}

func Test_GetAuthConfigs(t *testing.T) {
paulozenida marked this conversation as resolved.
Show resolved Hide resolved
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
1 change: 1 addition & 0 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st
buildOptions := types.ImageBuildOptions{
BuildArgs: img.GetBuildArgs(),
Dockerfile: img.GetDockerfile(),
AuthConfigs: img.GetAuthConfigs(),
Context: buildContext,
Tags: []string{repoTag},
Remove: true,
Expand Down
136 changes: 131 additions & 5 deletions docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (
nginxAlpineImage = "docker.io/nginx:alpine"
nginxDefaultPort = "80/tcp"
nginxHighPort = "8080/tcp"
daemonMaxVersion = "1.41"
)

var providerType = ProviderDocker
Expand Down Expand Up @@ -1033,22 +1034,147 @@ func Test_BuildContainerFromDockerfile(t *testing.T) {
WaitingFor: wait.ForLog("Ready to accept connections"),
}

t.Log("creating generic container request from container request")
redisC, err := prepareRedisImage(ctx, req, t)
require.NoError(t, err)
terminateContainerOnEnd(t, ctx, redisC)

checkSuccessfulRedisImage(ctx, redisC, t)

}

func Test_BuildContainerFromDockerfileWithAuthConfig_ShouldSucceedWithAuthConfigs(t *testing.T) {
prepareLocalRegistryWithAuth(t)
defer func() {
ctx := context.Background()
testcontainersClient, err := client.NewClientWithOpts(client.WithVersion(daemonMaxVersion))
if err != nil {
t.Log("could not create client to cleanup registry: ", err)
}

_, err = testcontainersClient.ImageRemove(ctx, "localhost:5000/redis:5.0-alpine", types.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
if err != nil {
t.Log("could not remove image: ", err)
}

}()

t.Log("getting context")
ctx := context.Background()
t.Log("got context, creating container request")
req := ContainerRequest{
FromDockerfile: FromDockerfile{
Context: "./testresources",
Dockerfile: "auth.Dockerfile",
AuthConfigs: map[string]types.AuthConfig{
"localhost:5000": {
Username: "testuser",
Password: "testpassword",
},
},
},

ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
}

redisC, err := prepareRedisImage(ctx, req, t)
require.NoError(t, err)
terminateContainerOnEnd(t, ctx, redisC)

checkSuccessfulRedisImage(ctx, redisC, t)
}

func Test_BuildContainerFromDockerfileWithAuthConfig_ShouldFailWithoutAuthConfigs(t *testing.T) {
prepareLocalRegistryWithAuth(t)

t.Log("getting context")
ctx := context.Background()
t.Log("got context, creating container request")
req := ContainerRequest{
FromDockerfile: FromDockerfile{
Context: "./testresources",
Dockerfile: "auth.Dockerfile",
},
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
}

redisC, err := prepareRedisImage(ctx, req, t)
require.Error(t, err)
terminateContainerOnEnd(t, ctx, redisC)
}

func prepareLocalRegistryWithAuth(t *testing.T) {
ctx := context.Background()
wd, err := os.Getwd()
mdelapenya marked this conversation as resolved.
Show resolved Hide resolved
assert.NoError(t, err)
req := ContainerRequest{
Image: "registry:2",
ExposedPorts: []string{"5000:5000/tcp"},
Env: map[string]string{
"REGISTRY_AUTH": "htpasswd",
"REGISTRY_AUTH_HTPASSWD_REALM": "Registry",
"REGISTRY_AUTH_HTPASSWD_PATH": "/auth/htpasswd",
"REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY": "/data",
},
Mounts: ContainerMounts{
ContainerMount{
Source: GenericBindMountSource{
HostPath: fmt.Sprintf("%s/testresources/auth", wd),
},
Target: "/auth",
},
ContainerMount{
Source: GenericBindMountSource{
HostPath: fmt.Sprintf("%s/testresources/data", wd),
},
Target: "/data",
},
},
WaitingFor: wait.ForExposedPort(),
}

genContainerReq := GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: req,
Started: true,
}

t.Log("creating registry container")

registryC, err := GenericContainer(ctx, genContainerReq)
assert.NoError(t, err)

t.Cleanup(func() {
assert.NoError(t, registryC.Terminate(context.Background()))
})

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
}

func prepareRedisImage(ctx context.Context, req ContainerRequest, t *testing.T) (Container, error) {
genContainerReq := GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: req,
Started: true,
}

t.Log("creating redis container")

redisC, err := GenericContainer(ctx, genContainerReq)
require.NoError(t, err)
terminateContainerOnEnd(t, ctx, redisC)

t.Log("created redis container")

return redisC, err
}

func checkSuccessfulRedisImage(ctx context.Context, redisC Container, t *testing.T) {
t.Log("created redis container")

t.Log("getting redis container endpoint")
endpoint, err := redisC.Endpoint(ctx, "")
if err != nil {
Expand All @@ -1057,12 +1183,12 @@ func Test_BuildContainerFromDockerfile(t *testing.T) {

t.Log("retrieved redis container endpoint")

client := redis.NewClient(&redis.Options{
redisClient := redis.NewClient(&redis.Options{
Addr: endpoint,
})

t.Log("pinging redis")
pong, err := client.Ping(ctx).Result()
pong, err := redisClient.Ping(ctx).Result()
require.NoError(t, err)

t.Log("received response from redis")
Expand Down
21 changes: 21 additions & 0 deletions docs/features/build_from_dockerfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,24 @@ fromDockerfile := testcontainers.FromDockerfile{

**Please Note** if you specify a `ContextArchive` this will cause Testcontainers-go to ignore the path passed
in to `Context`.

## Images requiring auth

If you are building a local Docker image that is fetched from a Docker image in a registry requiring authentication
(e.g., assuming you are fetching from a custom registry such as `myregistry.com`), you will need to specify the
credentials to succeed, as follows:

```go
req := ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: "/path/to/build/context",
Dockerfile: "CustomDockerfile",
AuthConfigs: map[string]types.AuthConfig{
"https://myregistry.com": {
Username: "myusername",
Password: "mypassword",
},
},
},
}
```
1 change: 1 addition & 0 deletions testresources/auth.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM localhost:5000/redis:5.0-alpine
2 changes: 2 additions & 0 deletions testresources/auth/htpasswd
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
testuser:$2y$05$tTymaYlWwJOqie.bcSUUN.I.kxmo1m5TLzYQ4/ejJ46UMXGtq78EO

Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 6320,
"digest": "sha256:960343481690ac146b55e1b704b9685104f1710bd557c7a409c48fdc721929fa"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2806054,
"digest": "sha256:213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1271,
"digest": "sha256:fb541f77610a7550755893b11853752742e9b173e4e9967f4db6b02c2e51ce4a"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 398361,
"digest": "sha256:dc2e3041aaa57579fe87bf26fbb56fcf7aef49b3f5a0e0ee37eab519855dd37e"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 6444723,
"digest": "sha256:a59c682d8704ef96c5dec2f59481ac24993fb3c25a4a139e22e2db664a34b06a"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 135,
"digest": "sha256:64acd9e2f7c70ed5b56219f5b8949a8cbc67c03f8c51fdb6786bec66eec4476b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 580,
"digest": "sha256:2595acf2c0bea71c4135062d027e4cf088b1674b3bb008b2571796491aa662b1"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"6379/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","REDIS_VERSION=5.0.14","REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.14.tar.gz","REDIS_DOWNLOAD_SHA=3ea5024766d983249e80d4aa9457c897a9f079957d0fb1f35682df233f997f32"],"Cmd":["redis-server"],"Image":"sha256:c6b61b4eb28dfbb356b1faf35269891803301551508b1604cf67492e23f58496","Volumes":{"/data":{}},"WorkingDir":"/data","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":null},"container":"3f5209e45fd8d25352646faf4c9ed85bd0f55581859dac5c0b6f71a0f354d59f","container_config":{"Hostname":"3f5209e45fd8","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"6379/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","REDIS_VERSION=5.0.14","REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.14.tar.gz","REDIS_DOWNLOAD_SHA=3ea5024766d983249e80d4aa9457c897a9f079957d0fb1f35682df233f997f32"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"redis-server\"]"],"Image":"sha256:c6b61b4eb28dfbb356b1faf35269891803301551508b1604cf67492e23f58496","Volumes":{"/data":{}},"WorkingDir":"/data","Entrypoint":["docker-entrypoint.sh"],"OnBuild":null,"Labels":{}},"created":"2022-10-07T03:33:38.951799853Z","docker_version":"20.10.12","history":[{"created":"2022-08-09T17:19:53.274069586Z","created_by":"/bin/sh -c #(nop) ADD file:2a949686d9886ac7c10582a6c29116fd29d3077d02755e87e111870d63607725 in / "},{"created":"2022-08-09T17:19:53.47374331Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true},{"created":"2022-10-07T03:30:17.540132626Z","created_by":"/bin/sh -c addgroup -S -g 1000 redis \u0026\u0026 adduser -S -G redis -u 999 redis"},{"created":"2022-10-07T03:30:18.701947002Z","created_by":"/bin/sh -c apk add --no-cache \t\t'su-exec\u003e=0.2' \t\ttzdata"},{"created":"2022-10-07T03:33:00.969689587Z","created_by":"/bin/sh -c #(nop) ENV REDIS_VERSION=5.0.14","empty_layer":true},{"created":"2022-10-07T03:33:01.061281294Z","created_by":"/bin/sh -c #(nop) ENV REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-5.0.14.tar.gz","empty_layer":true},{"created":"2022-10-07T03:33:01.154686334Z","created_by":"/bin/sh -c #(nop) ENV REDIS_DOWNLOAD_SHA=3ea5024766d983249e80d4aa9457c897a9f079957d0fb1f35682df233f997f32","empty_layer":true},{"created":"2022-10-07T03:33:37.807285887Z","created_by":"/bin/sh -c set -eux; \t\tapk add --no-cache --virtual .build-deps \t\tcoreutils \t\tdpkg-dev dpkg \t\tgcc \t\tlinux-headers \t\tmake \t\tmusl-dev \t\topenssl-dev \t\twget \t; \t\twget -O redis.tar.gz \"$REDIS_DOWNLOAD_URL\"; \techo \"$REDIS_DOWNLOAD_SHA *redis.tar.gz\" | sha256sum -c -; \tmkdir -p /usr/src/redis; \ttar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \trm redis.tar.gz; \t\tgrep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \tsed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\\1 0!' /usr/src/redis/src/server.h; \tgrep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \t\tgnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\"; \textraJemallocConfigureFlags=\"--build=$gnuArch\"; \tdpkgArch=\"$(dpkg --print-architecture)\"; \tcase \"${dpkgArch##*-}\" in \t\tamd64 | i386 | x32) extraJemallocConfigureFlags=\"$extraJemallocConfigureFlags --with-lg-page=12\" ;; \t\t*) extraJemallocConfigureFlags=\"$extraJemallocConfigureFlags --with-lg-page=16\" ;; \tesac; \textraJemallocConfigureFlags=\"$extraJemallocConfigureFlags --with-lg-hugepage=21\"; \tgrep -F 'cd jemalloc \u0026\u0026 ./configure ' /usr/src/redis/deps/Makefile; \tsed -ri 's!cd jemalloc \u0026\u0026 ./configure !\u0026'\"$extraJemallocConfigureFlags\"' !' /usr/src/redis/deps/Makefile; \tgrep -F \"cd jemalloc \u0026\u0026 ./configure $extraJemallocConfigureFlags \" /usr/src/redis/deps/Makefile; \t\tmake -C /usr/src/redis -j \"$(nproc)\" all; \tmake -C /usr/src/redis install; \t\tserverMd5=\"$(md5sum /usr/local/bin/redis-server | cut -d' ' -f1)\"; export serverMd5; \tfind /usr/local/bin/redis* -maxdepth 0 \t\t-type f -not -name redis-server \t\t-exec sh -eux -c ' \t\t\tmd5=\"$(md5sum \"$1\" | cut -d\" \" -f1)\"; \t\t\ttest \"$md5\" = \"$serverMd5\"; \t\t' -- '{}' ';' \t\t-exec ln -svfT 'redis-server' '{}' ';' \t; \t\trm -r /usr/src/redis; \t\trunDeps=\"$( \t\tscanelf --needed --nobanner --format '%n#p' --recursive /usr/local \t\t\t| tr ',' '\\n' \t\t\t| sort -u \t\t\t| awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' \t)\"; \tapk add --no-network --virtual .redis-rundeps $runDeps; \tapk del --no-network .build-deps; \t\tredis-cli --version; \tredis-server --version"},{"created":"2022-10-07T03:33:38.347225861Z","created_by":"/bin/sh -c mkdir /data \u0026\u0026 chown redis:redis /data"},{"created":"2022-10-07T03:33:38.444090076Z","created_by":"/bin/sh -c #(nop) VOLUME [/data]","empty_layer":true},{"created":"2022-10-07T03:33:38.545951015Z","created_by":"/bin/sh -c #(nop) WORKDIR /data","empty_layer":true},{"created":"2022-10-07T03:33:38.658945137Z","created_by":"/bin/sh -c #(nop) COPY file:a9e7249f657e2eec627bb4be492ad18aae3e5e1f0e47d22644eaf1ef2138c0ce in /usr/local/bin/ "},{"created":"2022-10-07T03:33:38.754618391Z","created_by":"/bin/sh -c #(nop) ENTRYPOINT [\"docker-entrypoint.sh\"]","empty_layer":true},{"created":"2022-10-07T03:33:38.850313099Z","created_by":"/bin/sh -c #(nop) EXPOSE 6379","empty_layer":true},{"created":"2022-10-07T03:33:38.951799853Z","created_by":"/bin/sh -c #(nop) CMD [\"redis-server\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7","sha256:6dbd9594c43d4115a12f2e203dfd586ba420dbd75a00d2d6c3feecdeb0048371","sha256:5669106330164180bda406cb49aa2126735bc29065b55354736d2656dffdbb96","sha256:ae23d15ebd31905b77598e96646e2cf46463bf8bd50e3b65c32ded7502402a9e","sha256:82566308f0b016b3848e916f16363ef5329f1fac362347fcb8cb99b1ba9461d7","sha256:beeee888b45e7cbe9e51c618ffe059806875e5570cb5607da75bd9e0b2649a43"]}}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:2595acf2c0bea71c4135062d027e4cf088b1674b3bb008b2571796491aa662b1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:64acd9e2f7c70ed5b56219f5b8949a8cbc67c03f8c51fdb6786bec66eec4476b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:960343481690ac146b55e1b704b9685104f1710bd557c7a409c48fdc721929fa
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:a59c682d8704ef96c5dec2f59481ac24993fb3c25a4a139e22e2db664a34b06a
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:dc2e3041aaa57579fe87bf26fbb56fcf7aef49b3f5a0e0ee37eab519855dd37e
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:fb541f77610a7550755893b11853752742e9b173e4e9967f4db6b02c2e51ce4a
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:662f040a4fc0e397e379a82a7d79663bb26698ae009d674e70b0224c4b155edb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:662f040a4fc0e397e379a82a7d79663bb26698ae009d674e70b0224c4b155edb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sha256:662f040a4fc0e397e379a82a7d79663bb26698ae009d674e70b0224c4b155edb