Skip to content

Commit

Permalink
fix(registry): compatibility with WSL
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stevenh committed Aug 8, 2024
1 parent 8b42143 commit a4b9771
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 233 deletions.
73 changes: 26 additions & 47 deletions modules/registry/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log"
"os"
"path/filepath"

"github.com/testcontainers/testcontainers-go"
Expand Down Expand Up @@ -39,9 +38,10 @@ func ExampleRun() {
}

func ExampleRun_withAuthentication() {
ctx := context.Background()
// htpasswdFile {
registryContainer, err := registry.Run(
context.Background(),
ctx,
"registry:2.8.3",
registry.WithHtpasswdFile(filepath.Join("testdata", "auth", "htpasswd")),
registry.WithData(filepath.Join("testdata", "data")),
Expand All @@ -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.
Expand All @@ -87,7 +75,7 @@ func ExampleRun_withAuthentication() {
FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join("testdata", "redis"),
BuildArgs: map[string]*string{
"REGISTRY_PORT": &strPort,
"REGISTRY_HOST": &registryHost,
},
PrintBuildLog: true,
},
Expand Down Expand Up @@ -118,47 +106,38 @@ 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")),
)
if err != nil {
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.
Expand All @@ -174,7 +153,7 @@ func ExampleRun_pushImage() {
FromDockerfile: testcontainers.FromDockerfile{
Context: filepath.Join("testdata", "redis"),
BuildArgs: map[string]*string{
"REGISTRY_PORT": &strPort,
"REGISTRY_HOST": &registryHost,
},
Repo: repo,
Tag: tag,
Expand Down
7 changes: 6 additions & 1 deletion modules/registry/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.0+incompatible
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.32.0
)

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 => ../..
9 changes: 9 additions & 0 deletions modules/registry/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
124 changes: 119 additions & 5 deletions modules/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,28 @@ 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"

"github.com/testcontainers/testcontainers-go"
"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
Expand All @@ -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 "", err
return "", fmt.Errorf("mapped port: %w", err)
}

host, err := c.Container.Host(ctx)
if err != nil {
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 fmt.Sprintf("http://%s:%s", ipAddress, port.Port()), nil
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
}

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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -203,3 +253,67 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom

return c, nil
}

// SetDockerAuthConfig sets the DOCKER_AUTH_CONFIG environment variable with
// authentication for with the given host, username and password.
// It returns the base64 encoded credentials and a function to reset back
// to the previous state.
func SetDockerAuthConfig(host, username, password string, additional ...string) (creds string, cleanup func(), err error) { //nolint:nonamedreturns // Adds context to the return values.
creds, auth, err := DockerAuthConfig(host, username, password, additional...)
if err != nil {
return "", nil, fmt.Errorf("docker auth config: %w", err)
}

previousAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG")
os.Setenv("DOCKER_AUTH_CONFIG", auth)

cleanup = func() {
if previousAuthConfig == "" {
os.Unsetenv("DOCKER_AUTH_CONFIG")
return
}
os.Setenv("DOCKER_AUTH_CONFIG", previousAuthConfig)
}

return creds, cleanup, nil
}

// DockerAuthConfig returns the base64 encoded credentials and the auth config
// 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) (creds, authConfig string, err error) { //nolint:nonamedreturns // Adds context to the return values.
if username != "" || password != "" {
creds = base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
}

cfg := dockercfg.Config{
AuthConfigs: map[string]dockercfg.AuthConfig{
host: {
Username: username,
Password: password,
Auth: creds,
},
},
CredentialsStore: "desktop",
}

if len(additional)%3 != 0 {
return "", "", fmt.Errorf("additional must be a multiple of 3")
}

for i := 0; i < len(additional); i += 3 {
host, username, password := additional[i], additional[i+1], additional[i+2]
cfg.AuthConfigs[host] = dockercfg.AuthConfig{
Username: username,
Password: password,
Auth: base64.StdEncoding.EncodeToString([]byte(username + ":" + password)),
}
}

data, err := json.Marshal(cfg)
if err != nil {
return "", "", fmt.Errorf("marshal auth config: %w", err)
}

return creds, string(data), nil
}
Loading

0 comments on commit a4b9771

Please sign in to comment.