Skip to content

Commit

Permalink
feat: Auth config for build images (#602)
Browse files Browse the repository at this point in the history
* add feature to allow configuration of auth configs. Update unit test and documentation accordingly

* copy go.sum from origin

* Revert "copy go.sum from origin"

This reverts commit ccdf903.

* add missing comment to newly added config

* organise imports

* add tests for authconfig

* removing docker-compose and relying only on a single docker container

* add the check to verify if the srcFile is a directory

* fix: apply same fix on tests for tar directory

Co-authored-by: Manuel de la Peña <mdelapenya@gmail.com>
  • Loading branch information
paulozenida and mdelapenya committed Nov 3, 2022
1 parent 63d83d1 commit cc9c80d
Show file tree
Hide file tree
Showing 26 changed files with 276 additions and 15 deletions.
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) {
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
139 changes: 134 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,8 +1034,129 @@ 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()
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,
Expand All @@ -1044,11 +1166,15 @@ func Test_BuildContainerFromDockerfile(t *testing.T) {
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 Expand Up @@ -2437,6 +2563,9 @@ func assertExtractedFiles(t *testing.T, ctx context.Context, container Container
require.NoError(t, err)

for _, srcFile := range srcFiles {
if srcFile.IsDir() {
continue
}
srcBytes, err := ioutil.ReadFile(filepath.Join(hostFilePath, srcFile.Name()))
if err != nil {
require.NoError(t, err)
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",
},
},
},
}
```
3 changes: 3 additions & 0 deletions file_test.go
Expand Up @@ -73,6 +73,9 @@ func Test_TarDir(t *testing.T) {
}

for _, srcFile := range srcFiles {
if srcFile.IsDir() {
continue
}
srcBytes, err := ioutil.ReadFile(filepath.Join(src, srcFile.Name()))
if err != nil {
t.Fatal(err)
Expand Down
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 comments on commit cc9c80d

Please sign in to comment.