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
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
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
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
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
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
@@ -0,0 +1 @@
FROM localhost:5000/redis:5.0-alpine
2 changes: 2 additions & 0 deletions testresources/auth/htpasswd
@@ -0,0 +1,2 @@
testuser:$2y$05$tTymaYlWwJOqie.bcSUUN.I.kxmo1m5TLzYQ4/ejJ46UMXGtq78EO

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
}
]
}
@@ -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.
@@ -0,0 +1 @@
sha256:213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49
@@ -0,0 +1 @@
sha256:2595acf2c0bea71c4135062d027e4cf088b1674b3bb008b2571796491aa662b1
@@ -0,0 +1 @@
sha256:64acd9e2f7c70ed5b56219f5b8949a8cbc67c03f8c51fdb6786bec66eec4476b
@@ -0,0 +1 @@
sha256:960343481690ac146b55e1b704b9685104f1710bd557c7a409c48fdc721929fa
@@ -0,0 +1 @@
sha256:a59c682d8704ef96c5dec2f59481ac24993fb3c25a4a139e22e2db664a34b06a
@@ -0,0 +1 @@
sha256:dc2e3041aaa57579fe87bf26fbb56fcf7aef49b3f5a0e0ee37eab519855dd37e
@@ -0,0 +1 @@
sha256:fb541f77610a7550755893b11853752742e9b173e4e9967f4db6b02c2e51ce4a
@@ -0,0 +1 @@
sha256:662f040a4fc0e397e379a82a7d79663bb26698ae009d674e70b0224c4b155edb
@@ -0,0 +1 @@
sha256:662f040a4fc0e397e379a82a7d79663bb26698ae009d674e70b0224c4b155edb
@@ -0,0 +1 @@
sha256:662f040a4fc0e397e379a82a7d79663bb26698ae009d674e70b0224c4b155edb