Skip to content

Commit 923cb7b

Browse files
committed
internal/core: add docker context inspect to host lookup strategies
This fixes the user-facing panic, covers additional tests that should be reported as non-fatal, and most importantly includes `docker context inxpect` in the docker host resolution strategies. Fixes #2952
1 parent 9b60a58 commit 923cb7b

File tree

3 files changed

+45
-17
lines changed

3 files changed

+45
-17
lines changed

internal/core/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package core
22

33
import (
44
"context"
5+
"fmt"
56
"path/filepath"
67

78
"github.com/docker/docker/client"
@@ -14,7 +15,10 @@ import (
1415
func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) {
1516
tcConfig := config.Read()
1617

17-
dockerHost := MustExtractDockerHost(ctx)
18+
dockerHost, err := ExtractDockerHost(ctx)
19+
if err != nil {
20+
return nil, fmt.Errorf("extract docker host: %w", err)
21+
}
1822

1923
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
2024
if dockerHost != "" {

internal/core/docker_host.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var (
2828
ErrSocketNotFoundInPath = errors.New("docker socket not found in " + DockerSocketPath)
2929
// ErrTestcontainersHostNotSetInProperties this error is specific to Testcontainers
3030
ErrTestcontainersHostNotSetInProperties = errors.New("tc.host not set in ~/.testcontainers.properties")
31+
ErrDockerSocketNotSetInDockerContext = errors.New("socket not found in docker context")
3132
)
3233

3334
var (
@@ -73,7 +74,7 @@ var dockerHostCheck = func(ctx context.Context, host string) error {
7374
return nil
7475
}
7576

76-
// MustExtractDockerHost Extracts the docker host from the different alternatives, caching the result to avoid unnecessary
77+
// MustExtractDockerHost extracts the docker host from the different alternatives, caching the result to avoid unnecessary
7778
// calculations. Use this function to get the actual Docker host. This function does not consider Windows containers at the moment.
7879
// The possible alternatives are:
7980
//
@@ -86,7 +87,7 @@ var dockerHostCheck = func(ctx context.Context, host string) error {
8687
// 7. Else, because the Docker host is not set, it panics.
8788
func MustExtractDockerHost(ctx context.Context) string {
8889
dockerHostOnce.Do(func() {
89-
cache, err := extractDockerHost(ctx)
90+
cache, err := ExtractDockerHost(ctx)
9091
if err != nil {
9192
panic(err)
9293
}
@@ -118,13 +119,22 @@ func MustExtractDockerSocket(ctx context.Context) string {
118119
return dockerSocketPathCache
119120
}
120121

121-
// extractDockerHost Extracts the docker host from the different alternatives, without caching the result.
122-
// This internal method is handy for testing purposes.
123-
func extractDockerHost(ctx context.Context) (string, error) {
122+
// ExtractDockerHost Extracts the docker host from the different alternatives, without caching the result.
123+
// Use this function to get the actual Docker host. This function does not consider Windows containers at the moment.
124+
// The possible alternatives are:
125+
//
126+
// 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file.
127+
// 2. DOCKER_HOST environment variable.
128+
// 3. Docker host from context.
129+
// 4. Docker host from the default docker socket path, without the unix schema.
130+
// 5. Docker host from the "docker.host" property in the ~/.testcontainers.properties file.
131+
// 6. Rootless docker socket path.
132+
func ExtractDockerHost(ctx context.Context) (string, error) {
124133
dockerHostFns := []func(context.Context) (string, error){
125134
testcontainersHostFromProperties,
126135
dockerHostFromEnv,
127136
dockerHostFromContext,
137+
dockerHostFromDockerContext,
128138
dockerSocketPath,
129139
dockerHostFromProperties,
130140
rootlessDockerSocketPath,
@@ -149,6 +159,7 @@ func extractDockerHost(ctx context.Context) (string, error) {
149159
}
150160

151161
if len(errs) > 0 {
162+
panic(32)
152163
return "", errors.Join(errs...)
153164
}
154165

@@ -212,7 +223,7 @@ func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) st
212223
return DockerSocketPath
213224
}
214225

215-
dockerHost, err := extractDockerHost(ctx)
226+
dockerHost, err := ExtractDockerHost(ctx)
216227
if err != nil {
217228
panic(err) // Docker host is required to get the Docker socket
218229
}
@@ -227,12 +238,14 @@ func isHostNotSet(err error) bool {
227238
case errors.Is(err, ErrTestcontainersHostNotSetInProperties),
228239
errors.Is(err, ErrDockerHostNotSet),
229240
errors.Is(err, ErrDockerSocketNotSetInContext),
241+
errors.Is(err, ErrDockerSocketNotSetInDockerContext),
230242
errors.Is(err, ErrDockerSocketNotSetInProperties),
231243
errors.Is(err, ErrSocketNotFoundInPath),
232244
errors.Is(err, ErrXDGRuntimeDirNotSet),
233245
errors.Is(err, ErrRootlessDockerNotFoundHomeRunDir),
234246
errors.Is(err, ErrRootlessDockerNotFoundHomeDesktopDir),
235-
errors.Is(err, ErrRootlessDockerNotFoundRunDir):
247+
errors.Is(err, ErrRootlessDockerNotFoundRunDir),
248+
errors.Is(err, ErrRootlessDockerNotFound):
236249
return true
237250
default:
238251
return false
@@ -262,6 +275,17 @@ func dockerHostFromContext(ctx context.Context) (string, error) {
262275
return "", ErrDockerSocketNotSetInContext
263276
}
264277

278+
// dockerHostFromDockerContext returns the docker host from the DOCKER_CONTEXT environment variable, if it's not empty
279+
func dockerHostFromDockerContext(_ context.Context) (string, error) {
280+
// exec `docker context inspect -f='{{.Endpoints.docker.Host}}'`
281+
cmd := exec.Command("docker", "context", "inspect", "-f", "{{.Endpoints.docker.Host}}")
282+
output, err := cmd.CombinedOutput()
283+
if err != nil {
284+
return "", fmt.Errorf("%w: %w", ErrDockerSocketNotSetInDockerContext, err)
285+
}
286+
return strings.TrimSpace(string(output)), nil
287+
}
288+
265289
// dockerHostFromProperties returns the docker host from the ~/.testcontainers.properties file, if it's not empty
266290
func dockerHostFromProperties(_ context.Context) (string, error) {
267291
cfg := config.Read()

internal/core/docker_host_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func TestExtractDockerHost(t *testing.T) {
8888

8989
setupTestcontainersProperties(t, content)
9090

91-
host, err := extractDockerHost(context.Background())
91+
host, err := ExtractDockerHost(context.Background())
9292
require.NoError(t, err)
9393
require.Equal(t, testRemoteHost, host)
9494
})
@@ -102,14 +102,14 @@ func TestExtractDockerHost(t *testing.T) {
102102
// mock the callback check to return an error
103103
mockCallbackCheck(t, testCallbackCheckError)
104104

105-
host, err := extractDockerHost(context.Background())
105+
host, err := ExtractDockerHost(context.Background())
106106
require.Error(t, err)
107107
require.Empty(t, host)
108108
})
109109

110110
t.Run("Docker Host as environment variable", func(t *testing.T) {
111111
t.Setenv("DOCKER_HOST", "/path/to/docker.sock")
112-
host, err := extractDockerHost(context.Background())
112+
host, err := ExtractDockerHost(context.Background())
113113
require.NoError(t, err)
114114
require.Equal(t, "/path/to/docker.sock", host)
115115
})
@@ -120,7 +120,7 @@ func TestExtractDockerHost(t *testing.T) {
120120

121121
ctx := context.Background()
122122

123-
host, err := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, "path-to-docker-sock"))
123+
host, err := ExtractDockerHost(context.WithValue(ctx, DockerHostContextKey, "path-to-docker-sock"))
124124
require.Error(t, err)
125125
require.Empty(t, host)
126126
})
@@ -130,15 +130,15 @@ func TestExtractDockerHost(t *testing.T) {
130130
setupRootlessNotFound(t)
131131
ctx := context.Background()
132132

133-
host, err := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, "http://path to docker sock"))
133+
host, err := ExtractDockerHost(context.WithValue(ctx, DockerHostContextKey, "http://path to docker sock"))
134134
require.Error(t, err)
135135
require.Empty(t, host)
136136
})
137137

138138
t.Run("Unix Docker Host is passed in context", func(t *testing.T) {
139139
ctx := context.Background()
140140

141-
host, err := extractDockerHost(context.WithValue(ctx, DockerHostContextKey, DockerSocketSchema+"/this/is/a/sample.sock"))
141+
host, err := ExtractDockerHost(context.WithValue(ctx, DockerHostContextKey, DockerSocketSchema+"/this/is/a/sample.sock"))
142142
require.NoError(t, err)
143143
require.Equal(t, "/this/is/a/sample.sock", host)
144144
})
@@ -150,7 +150,7 @@ func TestExtractDockerHost(t *testing.T) {
150150

151151
setupTestcontainersProperties(t, content)
152152

153-
host, err := extractDockerHost(context.Background())
153+
host, err := ExtractDockerHost(context.Background())
154154
require.NoError(t, err)
155155
require.Equal(t, DockerSocketSchema+"/this/is/a/sample.sock", host)
156156
})
@@ -159,15 +159,15 @@ func TestExtractDockerHost(t *testing.T) {
159159
setupRootlessNotFound(t)
160160
tmpSocket := setupDockerSocket(t)
161161

162-
host, err := extractDockerHost(context.Background())
162+
host, err := ExtractDockerHost(context.Background())
163163
require.NoError(t, err)
164164
require.Equal(t, tmpSocket, host)
165165
})
166166

167167
t.Run("Error when empty", func(t *testing.T) {
168168
setupDockerSocketNotFound(t)
169169
setupRootlessNotFound(t)
170-
host, err := extractDockerHost(context.Background())
170+
host, err := ExtractDockerHost(context.Background())
171171
require.Error(t, err)
172172
require.Empty(t, host)
173173
})

0 commit comments

Comments
 (0)