From c6d8a6c5f5a0a9c83f4cf60b16d1b98461f7c4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= Date: Wed, 28 Aug 2024 20:40:45 +0200 Subject: [PATCH] fix: check if the discovered docker socket responds (#2741) * fix: check if the discovered docker socket responds * fix: update tests * chore: add test * Revert "chore: add test" This reverts commit c6c4832a78138890cfd52543f3658a8d5ee62fd9. * Revert "fix: update tests" This reverts commit fbadada0697c9ac3c69049b77c68e4e231a507fb. * Revert "fix: check if the discovered docker socket responds" This reverts commit 19fb55bcf1f906655a6cbd5d64b8bb297a996b1a. * chore: support passing callback checks to the docker host/socket path resolution This way the tests are able to verify if the socket/host is reachable by calling a mock client. The production code will use the default callback check, which calls a vanilla docker client using the discovered host as host * chore: convert var into function * chore: mock callback check instead * chore: simplify * chore: raise error when extracting the docker host * docs: document that the extract functions panics * chore: simplify error handler * chore: use require.Panics * fix: remove duplicated case * fix: negotiate API version in the plain docker client call * fix: defer closing the client earlier * chore: better function name * chore: convert vars into functions * chore: no need to assert as panic should occur * chor: rename check function * chore: pass ctx to new function * chore: more exhaustive error check in tests * docs: typo * fix: update usage --- docker_client.go | 4 +- docs/features/configuration.md | 4 +- image_test.go | 4 +- internal/core/client.go | 2 +- internal/core/docker_host.go | 89 +++++++++++++--- internal/core/docker_host_test.go | 145 ++++++++++++++++++-------- internal/core/docker_rootless.go | 12 ++- internal/core/docker_rootless_test.go | 8 +- modules/localstack/localstack.go | 2 +- provider.go | 2 +- provider_test.go | 2 +- reaper.go | 2 +- reaper_test.go | 4 +- testcontainers.go | 15 ++- 14 files changed, 205 insertions(+), 90 deletions(-) diff --git a/docker_client.go b/docker_client.go index c8e8e825b0..04df712916 100644 --- a/docker_client.go +++ b/docker_client.go @@ -79,8 +79,8 @@ func (c *DockerClient) Info(ctx context.Context) (system.Info, error) { dockerInfo.OperatingSystem, dockerInfo.MemTotal/1024/1024, infoLabels, internal.Version, - core.ExtractDockerHost(ctx), - core.ExtractDockerSocket(ctx), + core.MustExtractDockerHost(ctx), + core.MustExtractDockerSocket(ctx), core.SessionID(), core.ProcessID(), ) diff --git a/docs/features/configuration.md b/docs/features/configuration.md index aa5a0d0a61..ee5b6a4d69 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -85,7 +85,7 @@ See [Docker environment variables](https://docs.docker.com/engine/reference/comm 3. `${HOME}/.docker/desktop/docker.sock`. 4. `/run/user/${UID}/docker.sock`, where `${UID}` is the user ID of the current user. -7. The default Docker socket including schema will be returned if none of the above are set. +7. The library panics if none of the above are set, meaning that the Docker host was not detected. ## Docker socket path detection @@ -109,4 +109,4 @@ Path to Docker's socket. Used by Ryuk, Docker Compose, and a few other container 6. Else, the default location of the docker socket is used: `/var/run/docker.sock` -In any case, if the docker socket schema is `tcp://`, the default docker socket path will be returned. +The library panics if the Docker host cannot be discovered. diff --git a/image_test.go b/image_test.go index 17595b6590..795a521b29 100644 --- a/image_test.go +++ b/image_test.go @@ -10,7 +10,7 @@ import ( ) func TestImageList(t *testing.T) { - t.Setenv("DOCKER_HOST", core.ExtractDockerHost(context.Background())) + t.Setenv("DOCKER_HOST", core.MustExtractDockerHost(context.Background())) provider, err := ProviderDocker.GetProvider() if err != nil { @@ -54,7 +54,7 @@ func TestImageList(t *testing.T) { } func TestSaveImages(t *testing.T) { - t.Setenv("DOCKER_HOST", core.ExtractDockerHost(context.Background())) + t.Setenv("DOCKER_HOST", core.MustExtractDockerHost(context.Background())) provider, err := ProviderDocker.GetProvider() if err != nil { diff --git a/internal/core/client.go b/internal/core/client.go index 64af509b9f..04a54bcbc5 100644 --- a/internal/core/client.go +++ b/internal/core/client.go @@ -14,7 +14,7 @@ import ( func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) { tcConfig := config.Read() - dockerHost := ExtractDockerHost(ctx) + dockerHost := MustExtractDockerHost(ctx) opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()} if dockerHost != "" { diff --git a/internal/core/docker_host.go b/internal/core/docker_host.go index b9d2d60d51..3088a3742b 100644 --- a/internal/core/docker_host.go +++ b/internal/core/docker_host.go @@ -56,7 +56,24 @@ func DefaultGatewayIP() (string, error) { return ip, nil } -// ExtractDockerHost Extracts the docker host from the different alternatives, caching the result to avoid unnecessary +// dockerHostCheck Use a vanilla Docker client to check if the Docker host is reachable. +// It will avoid recursive calls to this function. +var dockerHostCheck = func(ctx context.Context, host string) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(host), client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("new client: %w", err) + } + defer cli.Close() + + _, err = cli.Info(ctx) + if err != nil { + return fmt.Errorf("docker info: %w", err) + } + + return nil +} + +// MustExtractDockerHost Extracts the docker host from the different alternatives, caching the result to avoid unnecessary // calculations. Use this function to get the actual Docker host. This function does not consider Windows containers at the moment. // The possible alternatives are: // @@ -66,16 +83,21 @@ func DefaultGatewayIP() (string, error) { // 4. Docker host from the default docker socket path, without the unix schema. // 5. Docker host from the "docker.host" property in the ~/.testcontainers.properties file. // 6. Rootless docker socket path. -// 7. Else, the default Docker socket including schema will be returned. -func ExtractDockerHost(ctx context.Context) string { +// 7. Else, because the Docker host is not set, it panics. +func MustExtractDockerHost(ctx context.Context) string { dockerHostOnce.Do(func() { - dockerHostCache = extractDockerHost(ctx) + cache, err := extractDockerHost(ctx) + if err != nil { + panic(err) + } + + dockerHostCache = cache }) return dockerHostCache } -// ExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema and +// MustExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema and // caching the result to avoid unnecessary calculations. Use this function to get the docker socket path, // not the host (e.g. mounting the socket in a container). This function does not consider Windows containers at the moment. // The possible alternatives are: @@ -83,12 +105,12 @@ func ExtractDockerHost(ctx context.Context) string { // 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file. // 2. The TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable. // 3. Using a Docker client, check if the Info().OperativeSystem is "Docker Desktop" and return the default docker socket path for rootless docker. -// 4. Else, Get the current Docker Host from the existing strategies: see ExtractDockerHost. +// 4. Else, Get the current Docker Host from the existing strategies: see MustExtractDockerHost. // 5. If the socket contains the unix schema, the schema is removed (e.g. unix:///var/run/docker.sock -> /var/run/docker.sock) // 6. Else, the default location of the docker socket is used (/var/run/docker.sock) // -// In any case, if the docker socket schema is "tcp://", the default docker socket path will be returned. -func ExtractDockerSocket(ctx context.Context) string { +// It panics if a Docker client cannot be created, or the Docker host cannot be discovered. +func MustExtractDockerSocket(ctx context.Context) string { dockerSocketPathOnce.Do(func() { dockerSocketPathCache = extractDockerSocket(ctx) }) @@ -98,7 +120,7 @@ func ExtractDockerSocket(ctx context.Context) string { // extractDockerHost Extracts the docker host from the different alternatives, without caching the result. // This internal method is handy for testing purposes. -func extractDockerHost(ctx context.Context) string { +func extractDockerHost(ctx context.Context) (string, error) { dockerHostFns := []func(context.Context) (string, error){ testcontainersHostFromProperties, dockerHostFromEnv, @@ -108,25 +130,35 @@ func extractDockerHost(ctx context.Context) string { rootlessDockerSocketPath, } - outerErr := ErrSocketNotFound + var errs []error for _, dockerHostFn := range dockerHostFns { dockerHost, err := dockerHostFn(ctx) if err != nil { - outerErr = fmt.Errorf("%w: %w", outerErr, err) + if !isHostNotSet(err) { + errs = append(errs, err) + } continue } - return dockerHost + if err = dockerHostCheck(ctx, dockerHost); err != nil { + errs = append(errs, fmt.Errorf("check host %q: %w", dockerHost, err)) + continue + } + + return dockerHost, nil } - // We are not supporting Windows containers at the moment - return DockerSocketPathWithSchema + if len(errs) > 0 { + return "", errors.Join(errs...) + } + + return "", ErrSocketNotFound } -// extractDockerHost Extracts the docker socket from the different alternatives, without caching the result. +// extractDockerSocket Extracts the docker socket from the different alternatives, without caching the result. // It will internally use the default Docker client, calling the internal method extractDockerSocketFromClient with it. // This internal method is handy for testing purposes. -// If a Docker client cannot be created, the program will panic. +// It panics if a Docker client cannot be created, or the Docker host is not discovered. func extractDockerSocket(ctx context.Context) string { cli, err := NewClient(ctx) if err != nil { @@ -140,6 +172,7 @@ func extractDockerSocket(ctx context.Context) string { // extractDockerSocketFromClient Extracts the docker socket from the different alternatives, without caching the result, // and receiving an instance of the Docker API client interface. // This internal method is handy for testing purposes, passing a mock type simulating the desired behaviour. +// It panics if the Docker Info call errors, or the Docker host is not discovered. func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) string { // check that the socket is not a tcp or unix socket checkDockerSocketFn := func(socket string) string { @@ -179,11 +212,33 @@ func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) st return DockerSocketPath } - dockerHost := extractDockerHost(ctx) + dockerHost, err := extractDockerHost(ctx) + if err != nil { + panic(err) // Docker host is required to get the Docker socket + } return checkDockerSocketFn(dockerHost) } +// isHostNotSet returns true if the error is related to the Docker host +// not being set, false otherwise. +func isHostNotSet(err error) bool { + switch { + case errors.Is(err, ErrTestcontainersHostNotSetInProperties), + errors.Is(err, ErrDockerHostNotSet), + errors.Is(err, ErrDockerSocketNotSetInContext), + errors.Is(err, ErrDockerSocketNotSetInProperties), + errors.Is(err, ErrSocketNotFoundInPath), + errors.Is(err, ErrXDGRuntimeDirNotSet), + errors.Is(err, ErrRootlessDockerNotFoundHomeRunDir), + errors.Is(err, ErrRootlessDockerNotFoundHomeDesktopDir), + errors.Is(err, ErrRootlessDockerNotFoundRunDir): + return true + default: + return false + } +} + // dockerHostFromEnv returns the docker host from the DOCKER_HOST environment variable, if it's not empty func dockerHostFromEnv(ctx context.Context) (string, error) { if dockerHostPath := os.Getenv("DOCKER_HOST"); dockerHostPath != "" { diff --git a/internal/core/docker_host_test.go b/internal/core/docker_host_test.go index 23d8e1e1fa..eceee573fc 100644 --- a/internal/core/docker_host_test.go +++ b/internal/core/docker_host_test.go @@ -2,6 +2,7 @@ package core import ( "context" + "fmt" "os" "path/filepath" "testing" @@ -40,6 +41,22 @@ var resetSocketOverrideFn = func() { os.Setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", originalDockerSocketOverride) } +func testCallbackCheckPassing(_ context.Context, _ string) error { + return nil +} + +func testCallbackCheckError(_ context.Context, _ string) error { + return fmt.Errorf("could not check the Docker host") +} + +func mockCallbackCheck(t *testing.T, fn func(_ context.Context, _ string) error) { + oldCheck := dockerHostCheck + dockerHostCheck = fn + t.Cleanup(func() { + dockerHostCheck = oldCheck + }) +} + func TestExtractDockerHost(t *testing.T) { setupDockerHostNotFound(t) // do not mess with local .testcontainers.properties @@ -47,17 +64,21 @@ func TestExtractDockerHost(t *testing.T) { t.Setenv("HOME", tmpDir) t.Setenv("USERPROFILE", tmpDir) // Windows support - t.Run("Docker Host as extracted just once", func(t *testing.T) { + // apply the passing check to all sub-tests + mockCallbackCheck(t, testCallbackCheckPassing) + + t.Run("Docker Host is extracted just once", func(t *testing.T) { expected := "/path/to/docker.sock" t.Setenv("DOCKER_HOST", expected) - host := ExtractDockerHost(context.Background()) + + host := MustExtractDockerHost(context.Background()) assert.Equal(t, expected, host) t.Setenv("DOCKER_HOST", "/path/to/another/docker.sock") - host = ExtractDockerHost(context.Background()) - assert.Equal(t, expected, host) + host = MustExtractDockerHost(context.Background()) + require.Equal(t, expected, host) }) t.Run("Testcontainers Host is resolved first", func(t *testing.T) { @@ -66,16 +87,30 @@ func TestExtractDockerHost(t *testing.T) { setupTestcontainersProperties(t, content) - host := extractDockerHost(context.Background()) + host, err := extractDockerHost(context.Background()) + require.NoError(t, err) + require.Equal(t, testRemoteHost, host) + }) + + t.Run("Testcontainers Host is resolved first but not reachable", func(t *testing.T) { + t.Setenv("DOCKER_HOST", "/path/to/docker.sock") + content := "tc.host=" + testRemoteHost - assert.Equal(t, testRemoteHost, host) + setupTestcontainersProperties(t, content) + + // mock the callback check to return an error + mockCallbackCheck(t, testCallbackCheckError) + + host, err := extractDockerHost(context.Background()) + require.Error(t, err) + require.Equal(t, "", host) }) t.Run("Docker Host as environment variable", func(t *testing.T) { t.Setenv("DOCKER_HOST", "/path/to/docker.sock") - host := extractDockerHost(context.Background()) - - assert.Equal(t, "/path/to/docker.sock", host) + host, err := extractDockerHost(context.Background()) + require.NoError(t, err) + require.Equal(t, "/path/to/docker.sock", host) }) t.Run("Malformed Docker Host is passed in context", func(t *testing.T) { @@ -84,9 +119,9 @@ func TestExtractDockerHost(t *testing.T) { ctx := context.Background() - host := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, "path-to-docker-sock")) - - assert.Equal(t, DockerSocketPathWithSchema, host) + host, err := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, "path-to-docker-sock")) + require.Error(t, err) + require.Equal(t, "", host) }) t.Run("Malformed Schema Docker Host is passed in context", func(t *testing.T) { @@ -94,17 +129,17 @@ func TestExtractDockerHost(t *testing.T) { setupRootlessNotFound(t) ctx := context.Background() - host := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, "http://path to docker sock")) - - assert.Equal(t, DockerSocketPathWithSchema, host) + host, err := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, "http://path to docker sock")) + require.Error(t, err) + require.Equal(t, "", host) }) t.Run("Unix Docker Host is passed in context", func(t *testing.T) { ctx := context.Background() - host := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, DockerSocketSchema+"/this/is/a/sample.sock")) - - assert.Equal(t, "/this/is/a/sample.sock", host) + host, err := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, DockerSocketSchema+"/this/is/a/sample.sock")) + require.NoError(t, err) + require.Equal(t, "/this/is/a/sample.sock", host) }) t.Run("Unix Docker Host is passed as docker.host", func(t *testing.T) { @@ -114,26 +149,26 @@ func TestExtractDockerHost(t *testing.T) { setupTestcontainersProperties(t, content) - host := extractDockerHost(context.Background()) - - assert.Equal(t, DockerSocketSchema+"/this/is/a/sample.sock", host) + host, err := extractDockerHost(context.Background()) + require.NoError(t, err) + require.Equal(t, DockerSocketSchema+"/this/is/a/sample.sock", host) }) t.Run("Default Docker socket", func(t *testing.T) { setupRootlessNotFound(t) tmpSocket := setupDockerSocket(t) - host := extractDockerHost(context.Background()) - - assert.Equal(t, tmpSocket, host) + host, err := extractDockerHost(context.Background()) + require.NoError(t, err) + require.Equal(t, tmpSocket, host) }) - t.Run("Default Docker Host when empty", func(t *testing.T) { + t.Run("Error when empty", func(t *testing.T) { setupDockerSocketNotFound(t) setupRootlessNotFound(t) - host := extractDockerHost(context.Background()) - - assert.Equal(t, DockerSocketPathWithSchema, host) + host, err := extractDockerHost(context.Background()) + require.Error(t, err) + require.Equal(t, "", host) }) t.Run("Extract Docker socket", func(t *testing.T) { @@ -147,7 +182,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := testcontainersHostFromProperties(context.Background()) require.NoError(t, err) - assert.Equal(t, testRemoteHost, socket) + require.Equal(t, testRemoteHost, socket) }) t.Run("Testcontainers host is not defined in properties", func(t *testing.T) { @@ -169,7 +204,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromEnv(context.Background()) require.NoError(t, err) - assert.Equal(t, tmpSocket, socket) + require.Equal(t, tmpSocket, socket) }) t.Run("DOCKER_HOST is not set", func(t *testing.T) { @@ -191,7 +226,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerSocketOverridePath() require.NoError(t, err) - assert.Equal(t, tmpSocket, socket) + require.Equal(t, tmpSocket, socket) }) t.Run("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE is not set", func(t *testing.T) { @@ -209,7 +244,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromContext(context.WithValue(ctx, DockerHostContextKey, DockerSocketSchema+"/this/is/a/sample.sock")) require.NoError(t, err) - assert.Equal(t, "/this/is/a/sample.sock", socket) + require.Equal(t, "/this/is/a/sample.sock", socket) }) t.Run("Context sets a malformed Docker socket", func(t *testing.T) { @@ -233,7 +268,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerSocketPath(context.Background()) require.NoError(t, err) - assert.Equal(t, tmpSocket, socket) + require.Equal(t, tmpSocket, socket) }) t.Run("Docker host is defined in properties", func(t *testing.T) { @@ -244,7 +279,7 @@ func TestExtractDockerHost(t *testing.T) { socket, err := dockerHostFromProperties(context.Background()) require.NoError(t, err) - assert.Equal(t, tmpSocket, socket) + require.Equal(t, tmpSocket, socket) }) t.Run("Docker host is not defined in properties", func(t *testing.T) { @@ -285,13 +320,15 @@ func (m mockCli) Info(ctx context.Context) (system.Info, error) { func TestExtractDockerSocketFromClient(t *testing.T) { setupDockerHostNotFound(t) + mockCallbackCheck(t, testCallbackCheckPassing) + t.Run("Docker socket from Testcontainers host defined in properties", func(t *testing.T) { content := "tc.host=" + testRemoteHost setupTestcontainersProperties(t, content) socket := extractDockerSocketFromClient(context.Background(), mockCli{OS: "foo"}) - assert.Equal(t, DockerSocketPath, socket) + require.Equal(t, DockerSocketPath, socket) }) t.Run("Docker socket from Testcontainers host takes precedence over TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", func(t *testing.T) { @@ -303,7 +340,7 @@ func TestExtractDockerSocketFromClient(t *testing.T) { t.Setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/path/to/docker.sock") socket := extractDockerSocketFromClient(context.Background(), mockCli{OS: "foo"}) - assert.Equal(t, DockerSocketPath, socket) + require.Equal(t, DockerSocketPath, socket) }) t.Run("Docker Socket as Testcontainers environment variable", func(t *testing.T) { @@ -314,7 +351,7 @@ func TestExtractDockerSocketFromClient(t *testing.T) { t.Setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", "/path/to/docker.sock") host := extractDockerSocketFromClient(context.Background(), mockCli{OS: "foo"}) - assert.Equal(t, "/path/to/docker.sock", host) + require.Equal(t, "/path/to/docker.sock", host) }) t.Run("Docker Socket as Testcontainers environment variable, removes prefixes", func(t *testing.T) { @@ -324,11 +361,11 @@ func TestExtractDockerSocketFromClient(t *testing.T) { t.Setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", DockerSocketSchema+"/path/to/docker.sock") host := extractDockerSocketFromClient(context.Background(), mockCli{OS: "foo"}) - assert.Equal(t, "/path/to/docker.sock", host) + require.Equal(t, "/path/to/docker.sock", host) t.Setenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE", testRemoteHost) host = extractDockerSocketFromClient(context.Background(), mockCli{OS: "foo"}) - assert.Equal(t, DockerSocketPath, host) + require.Equal(t, DockerSocketPath, host) }) t.Run("Unix Docker Socket is passed as DOCKER_HOST variable (Docker Desktop on non-Windows)", func(t *testing.T) { @@ -347,7 +384,7 @@ func TestExtractDockerSocketFromClient(t *testing.T) { socket := extractDockerSocketFromClient(ctx, mockCli{OS: "Docker Desktop"}) - assert.Equal(t, DockerSocketPath, socket) + require.Equal(t, DockerSocketPath, socket) }) t.Run("Unix Docker Socket is passed as DOCKER_HOST variable (Docker Desktop for Windows)", func(t *testing.T) { @@ -362,7 +399,7 @@ func TestExtractDockerSocketFromClient(t *testing.T) { socket := extractDockerSocketFromClient(ctx, mockCli{OS: "Docker Desktop"}) - assert.Equal(t, WindowsDockerSocketPath, socket) + require.Equal(t, WindowsDockerSocketPath, socket) }) t.Run("Unix Docker Socket is passed as DOCKER_HOST variable (Not Docker Desktop)", func(t *testing.T) { @@ -376,7 +413,7 @@ func TestExtractDockerSocketFromClient(t *testing.T) { socket := extractDockerSocketFromClient(ctx, mockCli{OS: "Ubuntu"}) - assert.Equal(t, "/this/is/a/sample.sock", socket) + require.Equal(t, "/this/is/a/sample.sock", socket) }) t.Run("Unix Docker Socket is passed as DOCKER_HOST variable (Not Docker Desktop), removes prefixes", func(t *testing.T) { @@ -389,11 +426,11 @@ func TestExtractDockerSocketFromClient(t *testing.T) { t.Setenv("DOCKER_HOST", DockerSocketSchema+"/this/is/a/sample.sock") socket := extractDockerSocketFromClient(ctx, mockCli{OS: "Ubuntu"}) - assert.Equal(t, "/this/is/a/sample.sock", socket) + require.Equal(t, "/this/is/a/sample.sock", socket) t.Setenv("DOCKER_HOST", testRemoteHost) socket = extractDockerSocketFromClient(ctx, mockCli{OS: "Ubuntu"}) - assert.Equal(t, DockerSocketPath, socket) + require.Equal(t, DockerSocketPath, socket) }) t.Run("Unix Docker Socket is passed as docker.host property", func(t *testing.T) { @@ -409,7 +446,25 @@ func TestExtractDockerSocketFromClient(t *testing.T) { socket := extractDockerSocketFromClient(ctx, mockCli{OS: "Ubuntu"}) - assert.Equal(t, "/this/is/a/sample.sock", socket) + require.Equal(t, "/this/is/a/sample.sock", socket) + }) + + t.Run("Unix Docker Socket is passed as docker.host property but not reachable", func(t *testing.T) { + content := "docker.host=" + DockerSocketSchema + "/this/is/a/sample.sock" + setupTestcontainersProperties(t, content) + + t.Cleanup(resetSocketOverrideFn) + + ctx := context.Background() + os.Unsetenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE") + os.Unsetenv("DOCKER_HOST") + + mockCallbackCheck(t, testCallbackCheckError) + + require.Panics(t, func() { + // no need to check for the returned socket, as it must panic + _ = extractDockerSocketFromClient(ctx, mockCli{OS: "Ubuntu"}) + }) }) } diff --git a/internal/core/docker_rootless.go b/internal/core/docker_rootless.go index 44782d31b3..b8e0f6e17e 100644 --- a/internal/core/docker_rootless.go +++ b/internal/core/docker_rootless.go @@ -53,18 +53,24 @@ func rootlessDockerSocketPath(_ context.Context) (string, error) { rootlessSocketPathFromRunDir, } - outerErr := ErrRootlessDockerNotFound + var errs []error for _, socketPathFn := range socketPathFns { s, err := socketPathFn() if err != nil { - outerErr = fmt.Errorf("%w: %w", outerErr, err) + if !isHostNotSet(err) { + errs = append(errs, err) + } continue } return DockerSocketSchema + s, nil } - return "", outerErr + if len(errs) > 0 { + return "", errors.Join(errs...) + } + + return "", ErrRootlessDockerNotFound } func fileExists(f string) bool { diff --git a/internal/core/docker_rootless_test.go b/internal/core/docker_rootless_test.go index ef018eda53..7897f35783 100644 --- a/internal/core/docker_rootless_test.go +++ b/internal/core/docker_rootless_test.go @@ -178,14 +178,8 @@ func TestRootlessDockerSocketPath(t *testing.T) { setupRootlessNotFound(t) socketPath, err := rootlessDockerSocketPath(context.Background()) - require.ErrorIs(t, err, ErrRootlessDockerNotFound) + require.ErrorIs(t, err, ErrRootlessDockerNotFoundXDGRuntimeDir) assert.Empty(t, socketPath) - - // the wrapped error includes all the locations that were checked - require.ErrorContains(t, err, ErrRootlessDockerNotFoundXDGRuntimeDir.Error()) - require.ErrorContains(t, err, ErrRootlessDockerNotFoundHomeRunDir.Error()) - require.ErrorContains(t, err, ErrRootlessDockerNotFoundHomeDesktopDir.Error()) - require.ErrorContains(t, err, ErrRootlessDockerNotFoundRunDir.Error()) }) } diff --git a/modules/localstack/localstack.go b/modules/localstack/localstack.go index f06054e83b..961527cd3e 100644 --- a/modules/localstack/localstack.go +++ b/modules/localstack/localstack.go @@ -74,7 +74,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize // Run creates an instance of the LocalStack container type // - overrideReq: a function that can be used to override the default container request, usually used to set the image version, environment variables for localstack, etc. func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*LocalStackContainer, error) { - dockerHost := testcontainers.ExtractDockerSocket() + dockerHost := testcontainers.MustExtractDockerSocket(ctx) req := testcontainers.ContainerRequest{ Image: img, diff --git a/provider.go b/provider.go index c563503036..b5e5ffa997 100644 --- a/provider.go +++ b/provider.go @@ -147,7 +147,7 @@ func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error return &DockerProvider{ DockerProviderOptions: o, - host: core.ExtractDockerHost(ctx), + host: core.MustExtractDockerHost(ctx), client: c, config: config.Read(), }, nil diff --git a/provider_test.go b/provider_test.go index 2448d84c07..097c83a02c 100644 --- a/provider_test.go +++ b/provider_test.go @@ -8,7 +8,7 @@ import ( ) func TestProviderTypeGetProviderAutodetect(t *testing.T) { - dockerHost := core.ExtractDockerHost(context.Background()) + dockerHost := core.MustExtractDockerHost(context.Background()) const podmanSocket = "unix://$XDG_RUNTIME_DIR/podman/podman.sock" tests := []struct { diff --git a/reaper.go b/reaper.go index c17b4f329e..c41520b5b7 100644 --- a/reaper.go +++ b/reaper.go @@ -233,7 +233,7 @@ func reuseReaperContainer(ctx context.Context, sessionID string, provider Reaper // newReaper creates a Reaper with a sessionID to identify containers and a // provider to use. Do not call this directly, use reuseOrCreateReaper instead. func newReaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { - dockerHostMount := core.ExtractDockerSocket(ctx) + dockerHostMount := core.MustExtractDockerSocket(ctx) reaper := &Reaper{ Provider: provider, diff --git a/reaper_test.go b/reaper_test.go index c757fbb3ea..e526e8ec9a 100644 --- a/reaper_test.go +++ b/reaper_test.go @@ -91,7 +91,7 @@ func createContainerRequest(customize func(ContainerRequest) ContainerRequest) C ExposedPorts: []string{"8080/tcp"}, Labels: core.DefaultLabels(testSessionID), HostConfigModifier: func(hostConfig *container.HostConfig) { - hostConfig.Binds = []string{core.ExtractDockerSocket(context.Background()) + ":/var/run/docker.sock"} + hostConfig.Binds = []string{core.MustExtractDockerSocket(context.Background()) + ":/var/run/docker.sock"} }, WaitingFor: wait.ForListeningPort(nat.Port("8080/tcp")), Env: map[string]string{ @@ -376,7 +376,7 @@ func Test_NewReaper(t *testing.T) { name: "docker-host in context", req: createContainerRequest(func(req ContainerRequest) ContainerRequest { req.HostConfigModifier = func(hostConfig *container.HostConfig) { - hostConfig.Binds = []string{core.ExtractDockerSocket(context.Background()) + ":/var/run/docker.sock"} + hostConfig.Binds = []string{core.MustExtractDockerSocket(context.Background()) + ":/var/run/docker.sock"} } return req }), diff --git a/testcontainers.go b/testcontainers.go index 5b52e09a22..7ae4a40c14 100644 --- a/testcontainers.go +++ b/testcontainers.go @@ -6,7 +6,12 @@ import ( "github.com/testcontainers/testcontainers-go/internal/core" ) -// ExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema. +// Deprecated: use MustExtractDockerHost instead. +func ExtractDockerSocket() string { + return MustExtractDockerSocket(context.Background()) +} + +// MustExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema. // Use this function to get the docker socket path, not the host (e.g. mounting the socket in a container). // This function does not consider Windows containers at the moment. // The possible alternatives are: @@ -14,13 +19,13 @@ import ( // 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file. // 2. The TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable. // 3. Using a Docker client, check if the Info().OperativeSystem is "Docker Desktop" and return the default docker socket path for rootless docker. -// 4. Else, Get the current Docker Host from the existing strategies: see ExtractDockerHost. +// 4. Else, Get the current Docker Host from the existing strategies: see MustExtractDockerHost. // 5. If the socket contains the unix schema, the schema is removed (e.g. unix:///var/run/docker.sock -> /var/run/docker.sock) // 6. Else, the default location of the docker socket is used (/var/run/docker.sock) // -// In any case, if the docker socket schema is "tcp://", the default docker socket path will be returned. -func ExtractDockerSocket() string { - return core.ExtractDockerSocket(context.Background()) +// It panics if a Docker client cannot be created, or the Docker host cannot be discovered. +func MustExtractDockerSocket(ctx context.Context) string { + return core.MustExtractDockerSocket(ctx) } // SessionID returns a unique session ID for the current test session. Because each Go package