Skip to content

Commit 6aea26b

Browse files
committed
client: fix connection-errors being shadowed by API version mismatch errors
Commit e690724 applied a fix for situations where the client was configured with API-version negotiation, but did not yet negotiate a version. However, the checkVersion() function that was implemented copied the semantics of cli.NegotiateAPIVersion, which ignored connection failures with the assumption that connection errors would still surface further down. However, when using the result of a failed negotiation for NewVersionError, an API version mismatch error would be produced, masking the actual connection error. This patch changes the signature of checkVersion to return unexpected errors, including failures to connect to the API. Before this patch: docker -H unix:///no/such/socket.sock secret ls "secret list" requires API version 1.25, but the Docker daemon API version is 1.24 With this patch applied: docker -H unix:///no/such/socket.sock secret ls Cannot connect to the Docker daemon at unix:///no/such/socket.sock. Is the docker daemon running? Signed-off-by: Sebastiaan van Stijn <[email protected]>
1 parent 913478b commit 6aea26b

23 files changed

Lines changed: 179 additions & 26 deletions

client/client.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,17 +265,22 @@ func (cli *Client) Close() error {
265265
// This allows for version-dependent code to use the same version as will
266266
// be negotiated when making the actual requests, and for which cases
267267
// we cannot do the negotiation lazily.
268-
func (cli *Client) checkVersion(ctx context.Context) {
269-
if cli.negotiateVersion && !cli.negotiated {
270-
cli.NegotiateAPIVersion(ctx)
268+
func (cli *Client) checkVersion(ctx context.Context) error {
269+
if !cli.manualOverride && cli.negotiateVersion && !cli.negotiated {
270+
ping, err := cli.Ping(ctx)
271+
if err != nil {
272+
return err
273+
}
274+
cli.negotiateAPIVersionPing(ping)
271275
}
276+
return nil
272277
}
273278

274279
// getAPIPath returns the versioned request path to call the API.
275280
// It appends the query parameters to the path if they are not empty.
276281
func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string {
277282
var apiPath string
278-
cli.checkVersion(ctx)
283+
_ = cli.checkVersion(ctx)
279284
if cli.version != "" {
280285
v := strings.TrimPrefix(cli.version, "v")
281286
apiPath = path.Join(cli.basePath, "/v"+v, p)

client/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func TestNegotiateAPVersionOverride(t *testing.T) {
359359
func TestNegotiateAPVersionConnectionFailure(t *testing.T) {
360360
const expected = "9.99"
361361

362-
client, err := NewClientWithOpts(WithHost("unix:///no-such-socket"))
362+
client, err := NewClientWithOpts(WithHost("tcp://no-such-host.invalid"))
363363
assert.NilError(t, err)
364364

365365
client.version = expected

client/container_create.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
2828
//
2929
// Normally, version-negotiation (if enabled) would not happen until
3030
// the API request is made.
31-
cli.checkVersion(ctx)
31+
if err := cli.checkVersion(ctx); err != nil {
32+
return response, err
33+
}
3234

3335
if err := cli.NewVersionError(ctx, "1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil {
3436
return response, err

client/container_create_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,15 @@ func TestContainerCreateAutoRemove(t *testing.T) {
113113
t.Fatal(err)
114114
}
115115
}
116+
117+
// TestContainerCreateConnection verifies that connection errors occurring
118+
// during API-version negotiation are not shadowed by API-version errors.
119+
//
120+
// Regression test for https://github.com/docker/cli/issues/4890
121+
func TestContainerCreateConnectionError(t *testing.T) {
122+
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
123+
assert.NilError(t, err)
124+
125+
_, err = client.ContainerCreate(context.Background(), nil, nil, nil, nil, "")
126+
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
127+
}

client/container_exec.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ func (cli *Client) ContainerExecCreate(ctx context.Context, container string, co
1818
//
1919
// Normally, version-negotiation (if enabled) would not happen until
2020
// the API request is made.
21-
cli.checkVersion(ctx)
21+
if err := cli.checkVersion(ctx); err != nil {
22+
return response, err
23+
}
2224

2325
if err := cli.NewVersionError(ctx, "1.25", "env"); len(config.Env) != 0 && err != nil {
2426
return response, err

client/container_exec_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ func TestContainerExecCreateError(t *testing.T) {
2424
assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
2525
}
2626

27+
// TestContainerExecCreateConnectionError verifies that connection errors occurring
28+
// during API-version negotiation are not shadowed by API-version errors.
29+
//
30+
// Regression test for https://github.com/docker/cli/issues/4890
31+
func TestContainerExecCreateConnectionError(t *testing.T) {
32+
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
33+
assert.NilError(t, err)
34+
35+
_, err = client.ContainerExecCreate(context.Background(), "", types.ExecConfig{})
36+
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
37+
}
38+
2739
func TestContainerExecCreate(t *testing.T) {
2840
expectedURL := "/containers/container_id/exec"
2941
client := &Client{

client/container_restart.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ func (cli *Client) ContainerRestart(ctx context.Context, containerID string, opt
2323
//
2424
// Normally, version-negotiation (if enabled) would not happen until
2525
// the API request is made.
26-
cli.checkVersion(ctx)
26+
if err := cli.checkVersion(ctx); err != nil {
27+
return err
28+
}
2729
if versions.GreaterThanOrEqualTo(cli.version, "1.42") {
2830
query.Set("signal", options.Signal)
2931
}

client/container_restart_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ func TestContainerRestartError(t *testing.T) {
2323
assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
2424
}
2525

26+
// TestContainerRestartConnectionError verifies that connection errors occurring
27+
// during API-version negotiation are not shadowed by API-version errors.
28+
//
29+
// Regression test for https://github.com/docker/cli/issues/4890
30+
func TestContainerRestartConnectionError(t *testing.T) {
31+
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
32+
assert.NilError(t, err)
33+
34+
err = client.ContainerRestart(context.Background(), "nothing", container.StopOptions{})
35+
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
36+
}
37+
2638
func TestContainerRestart(t *testing.T) {
2739
const expectedURL = "/v1.42/containers/container_id/restart"
2840
client := &Client{

client/container_stop.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ func (cli *Client) ContainerStop(ctx context.Context, containerID string, option
2727
//
2828
// Normally, version-negotiation (if enabled) would not happen until
2929
// the API request is made.
30-
cli.checkVersion(ctx)
30+
if err := cli.checkVersion(ctx); err != nil {
31+
return err
32+
}
3133
if versions.GreaterThanOrEqualTo(cli.version, "1.42") {
3234
query.Set("signal", options.Signal)
3335
}

client/container_stop_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ func TestContainerStopError(t *testing.T) {
2323
assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
2424
}
2525

26+
// TestContainerStopConnectionError verifies that connection errors occurring
27+
// during API-version negotiation are not shadowed by API-version errors.
28+
//
29+
// Regression test for https://github.com/docker/cli/issues/4890
30+
func TestContainerStopConnectionError(t *testing.T) {
31+
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
32+
assert.NilError(t, err)
33+
34+
err = client.ContainerStop(context.Background(), "nothing", container.StopOptions{})
35+
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
36+
}
37+
2638
func TestContainerStop(t *testing.T) {
2739
const expectedURL = "/v1.42/containers/container_id/stop"
2840
client := &Client{

0 commit comments

Comments
 (0)