From f33cbc4fff443687cca28181bcd22020d911be8e Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Sat, 20 Jul 2024 11:03:54 +0100 Subject: [PATCH] fix: authentication tests on WSL Fix docker auth tests to run on WSL by using the local IP instead of localhost, which can be unreliable. While there refactor the tests to use a helper instead of hand coding each environment setup. Remove duplicate logging about terminating container as default hooks already provide this. --- docker_auth_test.go | 152 ++++++++++++++++++++------------------- testdata/auth.Dockerfile | 4 +- testhelpers_test.go | 1 - 3 files changed, 80 insertions(+), 77 deletions(-) diff --git a/docker_auth_test.go b/docker_auth_test.go index 5b3e1e1067..4e55d2b9bf 100644 --- a/docker_auth_test.go +++ b/docker_auth_test.go @@ -3,7 +3,9 @@ package testcontainers import ( "context" _ "embed" + "encoding/base64" "fmt" + "net" "os" "path/filepath" "testing" @@ -75,12 +77,7 @@ func TestGetDockerConfig(t *testing.T) { }) t.Run("DOCKER_AUTH_CONFIG env var takes precedence", func(t *testing.T) { - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "`+exampleAuth+`": {} - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, exampleAuth, "", "") t.Setenv("DOCKER_CONFIG", testDockerConfigDirPath) cfg, err := getDockerConfig() @@ -100,36 +97,23 @@ func TestGetDockerConfig(t *testing.T) { }) t.Run("retrieve auth with DOCKER_AUTH_CONFIG env var", func(t *testing.T) { - base64 := "Z29waGVyOnNlY3JldA==" // gopher:secret - - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "`+exampleAuth+`": { "username": "gopher", "password": "secret", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + username, password := "gopher", "secret" + creds := setAuthConfig(t, exampleAuth, username, password) registry, cfg, err := DockerImageAuth(context.Background(), exampleAuth+"/my/image:latest") require.NoError(t, err) require.NotEmpty(t, cfg) assert.Equal(t, exampleAuth, registry) - assert.Equal(t, "gopher", cfg.Username) - assert.Equal(t, "secret", cfg.Password) - assert.Equal(t, base64, cfg.Auth) + assert.Equal(t, username, cfg.Username) + assert.Equal(t, password, cfg.Password) + assert.Equal(t, creds, cfg.Auth) }) t.Run("match registry authentication by host", func(t *testing.T) { - base64 := "Z29waGVyOnNlY3JldA==" // gopher:secret imageReg := "example-auth.com" imagePath := "/my/image:latest" - - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "`+exampleAuth+`": { "username": "gopher", "password": "secret", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + base64 := setAuthConfig(t, exampleAuth, "gopher", "secret") registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath) require.NoError(t, err) @@ -142,17 +126,11 @@ func TestGetDockerConfig(t *testing.T) { }) t.Run("fail to match registry authentication due to invalid host", func(t *testing.T) { - base64 := "Z29waGVyOnNlY3JldA==" // gopher:secret imageReg := "example-auth.com" imagePath := "/my/image:latest" invalidRegistryURL := "://invalid-host" - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "`+invalidRegistryURL+`": { "username": "gopher", "password": "secret", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, invalidRegistryURL, "gopher", "secret") registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath) require.ErrorIs(t, err, dockercfg.ErrCredentialsNotFound) @@ -170,16 +148,10 @@ func TestGetDockerConfig(t *testing.T) { return "" } - base64 := "Z29waGVyOnNlY3JldA==" // gopher:secret imageReg := "" imagePath := "image:latest" - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "example-auth.com": { "username": "gopher", "password": "secret", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, "example-auth.com", "gopher", "secret") registry, cfg, err := DockerImageAuth(context.Background(), imageReg+imagePath) require.ErrorIs(t, err, dockercfg.ErrCredentialsNotFound) @@ -219,22 +191,16 @@ func removeImageFromLocalCache(t *testing.T, img string) { Force: true, PruneChildren: true, }) - if err != nil { + if err != nil && !client.IsErrNotFound(err) { t.Logf("could not remove image %s: %v\n", img, err) } } func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) { - mappedPort := prepareLocalRegistryWithAuth(t) + registryHost := prepareLocalRegistryWithAuth(t) // using the same credentials as in the Docker Registry - base64 := "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" // testuser:testpassword - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+mappedPort+`": { "username": "testuser", "password": "testpassword", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, registryHost, "testuser", "testpassword") ctx := context.Background() @@ -243,8 +209,10 @@ func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) { Context: "./testdata", Dockerfile: "auth.Dockerfile", BuildArgs: map[string]*string{ - "REGISTRY_PORT": &mappedPort, + "REGISTRY_HOST": ®istryHost, }, + Repo: "localhost", + PrintBuildLog: true, }, AlwaysPullImage: true, // make sure the authentication takes place ExposedPorts: []string{"6379/tcp"}, @@ -252,21 +220,15 @@ func TestBuildContainerFromDockerfileWithDockerAuthConfig(t *testing.T) { } redisC, err := prepareRedisImage(ctx, req) - require.NoError(t, err) terminateContainerOnEnd(t, ctx, redisC) + require.NoError(t, err) } func TestBuildContainerFromDockerfileShouldFailWithWrongDockerAuthConfig(t *testing.T) { - mappedPort := prepareLocalRegistryWithAuth(t) + registryHost := prepareLocalRegistryWithAuth(t) // using different credentials than in the Docker Registry - base64 := "Zm9vOmJhcg==" // foo:bar - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+mappedPort+`": { "username": "foo", "password": "bar", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, registryHost, "foo", "bar") ctx := context.Background() @@ -275,7 +237,7 @@ func TestBuildContainerFromDockerfileShouldFailWithWrongDockerAuthConfig(t *test Context: "./testdata", Dockerfile: "auth.Dockerfile", BuildArgs: map[string]*string{ - "REGISTRY_PORT": &mappedPort, + "REGISTRY_HOST": ®istryHost, }, }, AlwaysPullImage: true, // make sure the authentication takes place @@ -284,25 +246,19 @@ func TestBuildContainerFromDockerfileShouldFailWithWrongDockerAuthConfig(t *test } redisC, err := prepareRedisImage(ctx, req) - require.Error(t, err) terminateContainerOnEnd(t, ctx, redisC) + require.Error(t, err) } func TestCreateContainerFromPrivateRegistry(t *testing.T) { - mappedPort := prepareLocalRegistryWithAuth(t) + registryHost := prepareLocalRegistryWithAuth(t) // using the same credentials as in the Docker Registry - base64 := "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" // testuser:testpassword - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+mappedPort+`": { "username": "testuser", "password": "testpassword", "auth": "`+base64+`" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, registryHost, "testuser", "testpassword") ctx := context.Background() req := ContainerRequest{ - Image: "localhost:" + mappedPort + "/redis:5.0-alpine", + Image: registryHost + "/redis:5.0-alpine", AlwaysPullImage: true, // make sure the authentication takes place ExposedPorts: []string{"6379/tcp"}, WaitingFor: wait.ForLog("Ready to accept connections"), @@ -312,8 +268,8 @@ func TestCreateContainerFromPrivateRegistry(t *testing.T) { ContainerRequest: req, Started: true, }) - require.NoError(t, err) terminateContainerOnEnd(t, ctx, redisContainer) + require.NoError(t, err) } func prepareLocalRegistryWithAuth(t *testing.T) string { @@ -356,10 +312,12 @@ func prepareLocalRegistryWithAuth(t *testing.T) string { mappedPort, err := registryC.MappedPort(ctx, "5000/tcp") require.NoError(t, err) + ip := localAddress(t) mp := mappedPort.Port() + addr := ip + ":" + mp t.Cleanup(func() { - removeImageFromLocalCache(t, "localhost:"+mp+"/redis:5.0-alpine") + removeImageFromLocalCache(t, addr+"/redis:5.0-alpine") }) t.Cleanup(func() { require.NoError(t, registryC.Terminate(context.Background())) @@ -368,7 +326,7 @@ func prepareLocalRegistryWithAuth(t *testing.T) string { _, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - return mp + return addr } func prepareRedisImage(ctx context.Context, req ContainerRequest) (Container, error) { @@ -378,9 +336,55 @@ func prepareRedisImage(ctx context.Context, req ContainerRequest) (Container, er Started: true, } - redisC, err := GenericContainer(ctx, genContainerReq) + return GenericContainer(ctx, genContainerReq) +} + +// setAuthConfig sets the DOCKER_AUTH_CONFIG environment variable with +// authentication for with the given host, username and password. +// It returns the base64 encoded credentials. +func setAuthConfig(t *testing.T, host, username, password string) string { + t.Helper() + + var creds string + if username != "" || password != "" { + creds = base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + } + + auth := fmt.Sprintf(`{ + "auths": { + %q: { + "username": %q, + "password": %q, + "auth": %q + } + }, + "credsStore": "desktop" +}`, + host, + username, + password, + creds, + ) + t.Setenv("DOCKER_AUTH_CONFIG", auth) + + return creds +} + +// localAddress returns the local address of the machine +// which can be used to connect to the local registry. +// This avoids the issues with localhost on WSL. +func localAddress(t *testing.T) string { + if os.Getenv("WSL_DISTRO_NAME") == "" { + return "localhost" + } + + conn, err := net.Dial("udp", "golang.org:80") + require.NoError(t, err) + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) - return redisC, err + return localAddr.IP.String() } //go:embed testdata/.docker/config.json diff --git a/testdata/auth.Dockerfile b/testdata/auth.Dockerfile index 367e228a67..5fcebe0baf 100644 --- a/testdata/auth.Dockerfile +++ b/testdata/auth.Dockerfile @@ -1,3 +1,3 @@ -ARG REGISTRY_PORT=5001 +ARG REGISTRY_HOST=localhost:5001 -FROM localhost:${REGISTRY_PORT}/redis:5.0-alpine +FROM ${REGISTRY_HOST}/redis:5.0-alpine diff --git a/testhelpers_test.go b/testhelpers_test.go index 47bbcb54c3..3be3b7c50d 100644 --- a/testhelpers_test.go +++ b/testhelpers_test.go @@ -20,7 +20,6 @@ func terminateContainerOnEnd(tb testing.TB, ctx context.Context, ctr testcontain return } tb.Cleanup(func() { - tb.Log("terminating container") require.NoError(tb, ctr.Terminate(ctx)) }) }