From aaa88d20fea42734712bb22b646af33a47ed2f53 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Mon, 19 Aug 2024 13:21:45 +0100 Subject: [PATCH] fix(registry): compatibility with WSL (#2705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(registry): compatibility with WSL Use local IP instead of localhost to access registry container to address failures under WSL where the docker daemon can't access localhost. Refactor tests to use helpers and require reducing code and simplifying the flow. Add a default image which can be used be consumers. Add HostAddress which returns host : port compatible with WSL. Add SetDockerAuthConfig and DockerAuthConfig methods that can be used to easily configure authentication via DOCKER_AUTH_CONFIG environment variable. Fix container clean up which was missed in a number of case. * docs: document new functions * docs: wording * docs: wording Co-authored-by: Steven Hartland --------- Co-authored-by: Manuel de la Peña Co-authored-by: Manuel de la Peña --- docs/modules/registry.md | 18 +- modules/registry/examples_test.go | 73 ++---- modules/registry/go.mod | 7 +- modules/registry/go.sum | 9 + modules/registry/registry.go | 112 ++++++++- modules/registry/registry_test.go | 269 +++++++-------------- modules/registry/testdata/redis/Dockerfile | 4 +- 7 files changed, 258 insertions(+), 234 deletions(-) diff --git a/docs/modules/registry.md b/docs/modules/registry.md index ced274a2b6..94e7f4dc84 100644 --- a/docs/modules/registry.md +++ b/docs/modules/registry.md @@ -39,6 +39,17 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom - `string`, the Docker image to use. - `testcontainers.ContainerCustomizer`, a variadic argument for passing options. +### Docker Auth Config + +The module exposes a way to set the Docker Auth Config for the Registry container, thanks to the `SetDockerAuthConfig` function. +This is useful when you need to pull images from a private registry. It basically sets the `DOCKER_AUTH_CONFIG` environment variable +with authentication for the given host, username and password sets. It returns a function to reset the environment back to the previous state, +which is helpful when you need to reset the environment after a test. + +On the same hand, the module also exposes a way to build a Docker Auth Config for the Registry container, thanks to the `DockerAuthConfig` helper function. +This function returns a map of `AuthConfigs` including base64 encoded Auth field for the provided details. +It also accepts additional host, username and password triples to add more auth configurations. + ### Container Options When starting the Registry container, you can pass options in a variadic way to configure it. @@ -80,10 +91,15 @@ Otherwise, the Registry will start but you won't be able to read any images from The Registry container exposes the following methods: +#### HostAddress + +This method returns the returns the host address including port of the Distribution Registry. +E.g. `localhost:32878`. + #### Address This method returns the HTTP address string to connect to the Distribution Registry, so that you can use to connect to the Registry. -E.g. `http://localhost:32878/v2/_catalog`. +E.g. `http://localhost:32878`. [HTTP Address](../../modules/registry/registry_test.go) inside_block:httpAddress diff --git a/modules/registry/examples_test.go b/modules/registry/examples_test.go index 3b9f55099d..ada7e33b85 100644 --- a/modules/registry/examples_test.go +++ b/modules/registry/examples_test.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "path/filepath" "github.com/testcontainers/testcontainers-go" @@ -40,8 +39,9 @@ func ExampleRun() { func ExampleRun_withAuthentication() { // htpasswdFile { + ctx := context.Background() registryContainer, err := registry.Run( - context.Background(), + ctx, "registry:2.8.3", registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "data")), @@ -51,33 +51,21 @@ func ExampleRun_withAuthentication() { log.Fatalf("failed to start container: %s", err) } defer func() { - if err := registryContainer.Terminate(context.Background()); err != nil { + if err := registryContainer.Terminate(ctx); err != nil { log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic } }() - registryPort, err := registryContainer.MappedPort(context.Background(), "5000/tcp") + registryHost, err := registryContainer.HostAddress(ctx) if err != nil { - log.Fatalf("failed to get mapped port: %s", err) // nolint:gocritic + log.Fatalf("failed to get host: %s", err) // nolint:gocritic } - strPort := registryPort.Port() - - previousAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG") - // make sure the Docker Auth credentials are set - // using the same as in the Docker Registry - // testuser:testpassword - os.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+strPort+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } - }, - "credsStore": "desktop" - }`) - defer func() { - // reset the original state after the example. - os.Unsetenv("DOCKER_AUTH_CONFIG") - os.Setenv("DOCKER_AUTH_CONFIG", previousAuthConfig) - }() + cleanup, err := registry.SetDockerAuthConfig(registryHost, "testuser", "testpassword") + if err != nil { + log.Fatalf("failed to set docker auth config: %s", err) // nolint:gocritic + } + defer cleanup() // build a custom redis image from the private registry, // using RegistryName of the container as the registry. @@ -87,7 +75,7 @@ func ExampleRun_withAuthentication() { FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join("testdata", "redis"), BuildArgs: map[string]*string{ - "REGISTRY_PORT": &strPort, + "REGISTRY_HOST": ®istryHost, }, PrintBuildLog: true, }, @@ -118,9 +106,10 @@ func ExampleRun_withAuthentication() { } func ExampleRun_pushImage() { + ctx := context.Background() registryContainer, err := registry.Run( - context.Background(), - "registry:2.8.3", + ctx, + registry.DefaultImage, registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "data")), ) @@ -128,37 +117,27 @@ func ExampleRun_pushImage() { log.Fatalf("failed to start container: %s", err) } defer func() { - if err := registryContainer.Terminate(context.Background()); err != nil { + if err := registryContainer.Terminate(ctx); err != nil { log.Fatalf("failed to terminate container: %s", err) // nolint:gocritic } }() - registryPort, err := registryContainer.MappedPort(context.Background(), "5000/tcp") + registryHost, err := registryContainer.HostAddress(ctx) if err != nil { - log.Fatalf("failed to get mapped port: %s", err) // nolint:gocritic + log.Fatalf("failed to get host: %s", err) // nolint:gocritic } - strPort := registryPort.Port() - - previousAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG") - // make sure the Docker Auth credentials are set - // using the same as in the Docker Registry - // testuser:testpassword // Besides, we are also setting the authentication // for both the registry and localhost to make sure // the image is pushed to the private registry. - os.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+strPort+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" }, - "`+registryContainer.RegistryName+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } - }, - "credsStore": "desktop" - }`) - defer func() { - // reset the original state after the example. - os.Unsetenv("DOCKER_AUTH_CONFIG") - os.Setenv("DOCKER_AUTH_CONFIG", previousAuthConfig) - }() + cleanup, err := registry.SetDockerAuthConfig( + registryHost, "testuser", "testpassword", + registryContainer.RegistryName, "testuser", "testpassword", + ) + if err != nil { + log.Fatalf("failed to set docker auth config: %s", err) // nolint:gocritic + } + defer cleanup() // build a custom redis image from the private registry, // using RegistryName of the container as the registry. @@ -174,7 +153,7 @@ func ExampleRun_pushImage() { FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join("testdata", "redis"), BuildArgs: map[string]*string{ - "REGISTRY_PORT": &strPort, + "REGISTRY_HOST": ®istryHost, }, Repo: repo, Tag: tag, diff --git a/modules/registry/go.mod b/modules/registry/go.mod index 90513dc043..6aa887050c 100644 --- a/modules/registry/go.mod +++ b/modules/registry/go.mod @@ -3,7 +3,9 @@ module github.com/testcontainers/testcontainers-go/modules/registry go 1.21 require ( + github.com/cpuguy83/dockercfg v0.3.1 github.com/docker/docker v27.1.1+incompatible + github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.32.0 ) @@ -15,7 +17,7 @@ require ( github.com/containerd/containerd v1.7.18 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -26,6 +28,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -37,6 +40,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -53,6 +57,7 @@ require ( golang.org/x/sys v0.21.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/testcontainers/testcontainers-go => ../.. diff --git a/modules/registry/go.sum b/modules/registry/go.sum index 85338720c8..5583e9b0fc 100644 --- a/modules/registry/go.sum +++ b/modules/registry/go.sum @@ -16,6 +16,7 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,6 +53,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -78,6 +83,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -172,6 +179,8 @@ google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvy google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/registry/registry.go b/modules/registry/registry.go index 22e1aa1532..b554f8dcc7 100644 --- a/modules/registry/registry.go +++ b/modules/registry/registry.go @@ -5,10 +5,13 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net" "net/http" + "os" "strings" "time" + "github.com/cpuguy83/dockercfg" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" @@ -16,6 +19,14 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +const ( + // registryPort is the default port used by the Registry container. + registryPort = "5000/tcp" + + // DefaultImage is the default image used by the Registry container. + DefaultImage = "registry:2.8.3" +) + // RegistryContainer represents the Registry container type used in the module type RegistryContainer struct { testcontainers.Container @@ -24,17 +35,56 @@ type RegistryContainer struct { // Address returns the address of the Registry container, using the HTTP protocol func (c *RegistryContainer) Address(ctx context.Context) (string, error) { - port, err := c.MappedPort(ctx, "5000") + host, err := c.HostAddress(ctx) if err != nil { return "", err } - ipAddress, err := c.Host(ctx) + return "http://" + host, nil +} + +// HostAddress returns the host address including port of the Registry container. +func (c *RegistryContainer) HostAddress(ctx context.Context) (string, error) { + port, err := c.MappedPort(ctx, registryPort) + if err != nil { + return "", fmt.Errorf("mapped port: %w", err) + } + + host, err := c.Container.Host(ctx) if err != nil { - return "", err + return "", fmt.Errorf("host: %w", err) + } + + if host == "localhost" { + // This is a workaround for WSL, where localhost is not reachable from Docker. + host, err = localAddress(ctx) + if err != nil { + return "", fmt.Errorf("local ip: %w", err) + } + } + + return net.JoinHostPort(host, port.Port()), nil +} + +// 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(ctx context.Context) (string, error) { + if os.Getenv("WSL_DISTRO_NAME") == "" { + return "localhost", nil } - return fmt.Sprintf("http://%s:%s", ipAddress, port.Port()), nil + var d net.Dialer + conn, err := d.DialContext(ctx, "udp", "golang.org:80") + if err != nil { + return "", fmt.Errorf("dial: %w", err) + } + + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP.String(), nil } // getEndpointWithAuth returns the HTTP endpoint of the Registry container, along with the image auth @@ -165,7 +215,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*RegistryContainer, error) { req := testcontainers.ContainerRequest{ Image: img, - ExposedPorts: []string{"5000/tcp"}, + ExposedPorts: []string{registryPort}, Env: map[string]string{ // convenient for testing "REGISTRY_STORAGE_DELETE_ENABLED": "true", @@ -203,3 +253,55 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom return c, nil } + +// SetDockerAuthConfig sets the DOCKER_AUTH_CONFIG environment variable with +// authentication for the given host, username and password sets. +// It returns a function to reset the environment back to the previous state. +func SetDockerAuthConfig(host, username, password string, additional ...string) (func(), error) { + authConfigs, err := DockerAuthConfig(host, username, password, additional...) + if err != nil { + return nil, fmt.Errorf("docker auth config: %w", err) + } + + auth, err := json.Marshal(dockercfg.Config{AuthConfigs: authConfigs}) + if err != nil { + return nil, fmt.Errorf("marshal auth config: %w", err) + } + + previousAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG") + os.Setenv("DOCKER_AUTH_CONFIG", string(auth)) + + return func() { + if previousAuthConfig == "" { + os.Unsetenv("DOCKER_AUTH_CONFIG") + return + } + os.Setenv("DOCKER_AUTH_CONFIG", previousAuthConfig) + }, nil +} + +// DockerAuthConfig returns a map of AuthConfigs including base64 encoded Auth field +// for the provided details. It also accepts additional host, username and password +// triples to add more auth configurations. +func DockerAuthConfig(host, username, password string, additional ...string) (map[string]dockercfg.AuthConfig, error) { + if len(additional)%3 != 0 { + return nil, fmt.Errorf("additional must be a multiple of 3") + } + + additional = append(additional, host, username, password) + authConfigs := make(map[string]dockercfg.AuthConfig, len(additional)/3) + for i := 0; i < len(additional); i += 3 { + host, username, password := additional[i], additional[i+1], additional[i+2] + auth := dockercfg.AuthConfig{ + Username: username, + Password: password, + } + + if username != "" || password != "" { + auth.Auth = base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + } + authConfigs[host] = auth + } + + return authConfigs, nil +} diff --git a/modules/registry/registry_test.go b/modules/registry/registry_test.go index dd071ef5be..d40f75125e 100644 --- a/modules/registry/registry_test.go +++ b/modules/registry/registry_test.go @@ -2,147 +2,103 @@ package registry_test import ( "context" + "encoding/json" "net/http" "path/filepath" - "strings" "testing" + "github.com/cpuguy83/dockercfg" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/registry" "github.com/testcontainers/testcontainers-go/wait" ) func TestRegistry_unauthenticated(t *testing.T) { - container, err := registry.Run(context.Background(), "registry:2.8.3") - if err != nil { - t.Fatal(err) - } + ctx := context.Background() + container, err := registry.Run(ctx, registry.DefaultImage) + terminateContainerOnEnd(t, ctx, container) + require.NoError(t, err) - // Clean up the container after the test is complete - t.Cleanup(func() { - if err := container.Terminate(context.Background()); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - - httpAddress, err := container.Address(context.Background()) - if err != nil { - t.Fatal(err) - } + httpAddress, err := container.Address(ctx) + require.NoError(t, err) resp, err := http.Get(httpAddress + "/v2/_catalog") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("expected status code 200, but got %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) } func TestRunContainer_authenticated(t *testing.T) { + ctx := context.Background() registryContainer, err := registry.Run( - context.Background(), - "registry:2.8.3", + ctx, + registry.DefaultImage, registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "data")), ) - if err != nil { - t.Fatalf("failed to start container: %s", err) - } - t.Cleanup(func() { - if err := registryContainer.Terminate(context.Background()); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + terminateContainerOnEnd(t, ctx, registryContainer) + require.NoError(t, err) // httpAddress { - httpAddress, err := registryContainer.Address(context.Background()) + httpAddress, err := registryContainer.Address(ctx) // } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - registryPort, err := registryContainer.MappedPort(context.Background(), "5000/tcp") - if err != nil { - t.Fatalf("failed to get mapped port: %s", err) - } - strPort := registryPort.Port() + registryHost, err := registryContainer.HostAddress(ctx) + require.NoError(t, err) t.Run("HTTP connection without basic auth fails", func(tt *testing.T) { httpCli := http.Client{} - req, err := http.NewRequest("GET", httpAddress+"/v2/_catalog", nil) - if err != nil { - tt.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, httpAddress+"/v2/_catalog", nil) + require.NoError(t, err) resp, err := httpCli.Do(req) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusUnauthorized { - t.Fatalf("expected status code 401, but got %d", resp.StatusCode) - } + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) t.Run("HTTP connection with incorrect basic auth fails", func(tt *testing.T) { httpCli := http.Client{} - req, err := http.NewRequest("GET", httpAddress+"/v2/_catalog", nil) - if err != nil { - tt.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, httpAddress+"/v2/_catalog", nil) + require.NoError(t, err) req.SetBasicAuth("foo", "bar") resp, err := httpCli.Do(req) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusUnauthorized { - t.Fatalf("expected status code 401, but got %d", resp.StatusCode) - } + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) t.Run("HTTP connection with basic auth succeeds", func(tt *testing.T) { httpCli := http.Client{} - req, err := http.NewRequest("GET", httpAddress+"/v2/_catalog", nil) - if err != nil { - tt.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, httpAddress+"/v2/_catalog", nil) + require.NoError(t, err) req.SetBasicAuth("testuser", "testpassword") resp, err := httpCli.Do(req) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("expected status code 200, but got %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("build images with wrong credentials fails", func(tt *testing.T) { - // Zm9vOmJhcg== is base64 for foo:bar - tt.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+strPort+`": { "username": "foo", "password": "bar", "auth": "Zm9vOmJhcg==" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(tt, registryHost, "foo", "bar") redisC, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ ContainerRequest: testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join("testdata", "redis"), BuildArgs: map[string]*string{ - "REGISTRY_PORT": &strPort, + "REGISTRY_HOST": ®istryHost, }, }, AlwaysPullImage: true, // make sure the authentication takes place @@ -151,31 +107,13 @@ func TestRunContainer_authenticated(t *testing.T) { }, Started: true, }) - if err == nil { - tt.Fatalf("expected to fail to start container, but it did not") - } - if redisC != nil { - tt.Fatal("redis container should not be running") - tt.Cleanup(func() { - if err := redisC.Terminate(context.Background()); err != nil { - tt.Fatalf("failed to terminate container: %s", err) - } - }) - } - - if !strings.Contains(err.Error(), "unauthorized: authentication required") { - tt.Fatalf("expected error to be 'unauthorized: authentication required' but got '%s'", err.Error()) - } + terminateContainerOnEnd(tt, ctx, redisC) + require.Error(tt, err) + require.Contains(tt, err.Error(), "unauthorized: authentication required") }) t.Run("build image with valid credentials", func(tt *testing.T) { - // dGVzdHVzZXI6dGVzdHBhc3N3b3Jk is base64 for testuser:testpassword - tt.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+strPort+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(tt, registryHost, "testuser", "testpassword") // build a custom redis image from the private registry, // using RegistryName of the container as the registry. @@ -187,7 +125,7 @@ func TestRunContainer_authenticated(t *testing.T) { FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join("testdata", "redis"), BuildArgs: map[string]*string{ - "REGISTRY_PORT": &strPort, + "REGISTRY_HOST": ®istryHost, }, }, AlwaysPullImage: true, // make sure the authentication takes place @@ -196,97 +134,58 @@ func TestRunContainer_authenticated(t *testing.T) { }, Started: true, }) - if err != nil { - tt.Fatalf("failed to start container: %s", err) - } - - tt.Cleanup(func() { - if err := redisC.Terminate(context.Background()); err != nil { - tt.Fatalf("failed to terminate container: %s", err) - } - }) + terminateContainerOnEnd(tt, ctx, redisC) + require.NoError(tt, err) state, err := redisC.State(context.Background()) - if err != nil { - tt.Fatalf("failed to get redis container state: %s", err) // nolint:gocritic - } - - if !state.Running { - tt.Fatalf("expected redis container to be running, but it is not") - } + require.NoError(tt, err) + require.True(tt, state.Running, "expected redis container to be running, but it is not") }) } func TestRunContainer_authenticated_withCredentials(t *testing.T) { + ctx := context.Background() // htpasswdString { registryContainer, err := registry.Run( - context.Background(), - "registry:2.8.3", + ctx, + registry.DefaultImage, registry.WithHtpasswd("testuser:$2y$05$tTymaYlWwJOqie.bcSUUN.I.kxmo1m5TLzYQ4/ejJ46UMXGtq78EO"), ) // } - if err != nil { - t.Fatalf("failed to start container: %s", err) - } - t.Cleanup(func() { - if err := registryContainer.Terminate(context.Background()); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + terminateContainerOnEnd(t, ctx, registryContainer) + require.NoError(t, err) - httpAddress, err := registryContainer.Address(context.Background()) - if err != nil { - t.Fatal(err) - } + httpAddress, err := registryContainer.Address(ctx) + require.NoError(t, err) httpCli := http.Client{} - req, err := http.NewRequest("GET", httpAddress+"/v2/_catalog", nil) - if err != nil { - t.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, httpAddress+"/v2/_catalog", nil) + require.NoError(t, err) req.SetBasicAuth("testuser", "testpassword") resp, err := httpCli.Do(req) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("expected status code 200, but got %d", resp.StatusCode) - } + require.Equal(t, http.StatusOK, resp.StatusCode) } func TestRunContainer_wrongData(t *testing.T) { + ctx := context.Background() registryContainer, err := registry.Run( - context.Background(), - "registry:2.8.3", + ctx, + registry.DefaultImage, registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")), registry.WithData(filepath.Join("testdata", "wrongdata")), ) - if err != nil { - t.Fatalf("failed to start container: %s", err) - } - t.Cleanup(func() { - if err := registryContainer.Terminate(context.Background()); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) + terminateContainerOnEnd(t, ctx, registryContainer) + require.NoError(t, err) - registryPort, err := registryContainer.MappedPort(context.Background(), "5000/tcp") - if err != nil { - t.Fatalf("failed to get mapped port: %s", err) - } - strPort := registryPort.Port() + registryHost, err := registryContainer.HostAddress(ctx) + require.NoError(t, err) - // dGVzdHVzZXI6dGVzdHBhc3N3b3Jk is base64 for testuser:testpassword - t.Setenv("DOCKER_AUTH_CONFIG", `{ - "auths": { - "localhost:`+strPort+`": { "username": "testuser", "password": "testpassword", "auth": "dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } - }, - "credsStore": "desktop" - }`) + setAuthConfig(t, registryHost, "testuser", "testpassword") // build a custom redis image from the private registry, // using RegistryName of the container as the registry. @@ -298,7 +197,7 @@ func TestRunContainer_wrongData(t *testing.T) { FromDockerfile: testcontainers.FromDockerfile{ Context: filepath.Join("testdata", "redis"), BuildArgs: map[string]*string{ - "REGISTRY_PORT": &strPort, + "REGISTRY_HOST": ®istryHost, }, }, AlwaysPullImage: true, // make sure the authentication takes place @@ -307,19 +206,33 @@ func TestRunContainer_wrongData(t *testing.T) { }, Started: true, }) - if err == nil { - t.Fatalf("expected to fail to start container, but it did not") - } - if redisC != nil { - t.Fatal("redis container should not be running") - t.Cleanup(func() { - if err := redisC.Terminate(context.Background()); err != nil { - t.Fatalf("failed to terminate container: %s", err) - } - }) - } + terminateContainerOnEnd(t, ctx, redisC) + require.Error(t, err) + require.Contains(t, err.Error(), "manifest unknown") +} + +// setAuthConfig sets the DOCKER_AUTH_CONFIG environment variable with +// authentication for with the given host, username and password. +func setAuthConfig(t *testing.T, host, username, password string) { + t.Helper() - if !strings.Contains(err.Error(), "manifest unknown") { - t.Fatalf("expected error to be 'manifest unknown' but got '%s'", err.Error()) + authConfigs, err := registry.DockerAuthConfig(host, username, password) + require.NoError(t, err) + auth, err := json.Marshal(dockercfg.Config{AuthConfigs: authConfigs}) + require.NoError(t, err) + + t.Setenv("DOCKER_AUTH_CONFIG", string(auth)) +} + +// terminateContainerOnEnd terminates the container when the test ends if it is not nil. +func terminateContainerOnEnd(tb testing.TB, ctx context.Context, container testcontainers.Container) { + tb.Helper() + + if container == nil { + return } + + tb.Cleanup(func() { + require.NoError(tb, container.Terminate(ctx)) + }) } diff --git a/modules/registry/testdata/redis/Dockerfile b/modules/registry/testdata/redis/Dockerfile index 502db64261..280c33c827 100644 --- a/modules/registry/testdata/redis/Dockerfile +++ b/modules/registry/testdata/redis/Dockerfile @@ -1,3 +1,3 @@ -ARG REGISTRY_PORT=5000 +ARG REGISTRY_HOST=localhost:5000 -FROM localhost:${REGISTRY_PORT}/redis:5.0-alpine \ No newline at end of file +FROM ${REGISTRY_HOST}/redis:5.0-alpine