Skip to content

Commit

Permalink
fix: check if the discovered docker socket responds (testcontainers#2741
Browse files Browse the repository at this point in the history
)

* fix: check if the discovered docker socket responds

* fix: update tests

* chore: add test

* Revert "chore: add test"

This reverts commit c6c4832.

* Revert "fix: update tests"

This reverts commit fbadada.

* Revert "fix: check if the discovered docker socket responds"

This reverts commit 19fb55b.

* 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
  • Loading branch information
mdelapenya authored Aug 28, 2024
1 parent ef4243e commit c6d8a6c
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 90 deletions.
4 changes: 2 additions & 2 deletions docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down
4 changes: 2 additions & 2 deletions docs/features/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
4 changes: 2 additions & 2 deletions image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/core/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
89 changes: 72 additions & 17 deletions internal/core/docker_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
Expand All @@ -66,29 +83,34 @@ 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:
//
// 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)
})
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 != "" {
Expand Down
Loading

0 comments on commit c6d8a6c

Please sign in to comment.