Skip to content

Commit e95f6c8

Browse files
Merge pull request moby#51307 from thaJeztah/ping_once
client: remove NegotiateAPIVersion, NegotiateAPIVersionPing
2 parents 90109a3 + edbf321 commit e95f6c8

File tree

7 files changed

+164
-161
lines changed

7 files changed

+164
-161
lines changed

client/client.go

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -259,23 +259,13 @@ func (cli *Client) Close() error {
259259
// be negotiated when making the actual requests, and for which cases
260260
// we cannot do the negotiation lazily.
261261
func (cli *Client) checkVersion(ctx context.Context) error {
262-
if !cli.manualOverride && cli.negotiateVersion && !cli.negotiated.Load() {
263-
// Ensure exclusive write access to version and negotiated fields
264-
cli.negotiateLock.Lock()
265-
defer cli.negotiateLock.Unlock()
266-
267-
// May have been set during last execution of critical zone
268-
if cli.negotiated.Load() {
269-
return nil
270-
}
271-
272-
ping, err := cli.Ping(ctx, PingOptions{})
273-
if err != nil {
274-
return err
275-
}
276-
return cli.negotiateAPIVersion(ping.APIVersion)
262+
if cli.manualOverride || !cli.negotiateVersion || cli.negotiated.Load() {
263+
return nil
277264
}
278-
return nil
265+
_, err := cli.Ping(ctx, PingOptions{
266+
NegotiateAPIVersion: true,
267+
})
268+
return err
279269
}
280270

281271
// getAPIPath returns the versioned request path to call the API.
@@ -296,59 +286,6 @@ func (cli *Client) ClientVersion() string {
296286
return cli.version
297287
}
298288

299-
// NegotiateAPIVersion queries the API and updates the version to match the API
300-
// version. NegotiateAPIVersion downgrades the client's API version to match the
301-
// APIVersion if the ping version is lower than the default version. If the API
302-
// version reported by the server is higher than the maximum version supported
303-
// by the client, it uses the client's maximum version.
304-
//
305-
// If a manual override is in place, either through the "DOCKER_API_VERSION"
306-
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
307-
// with a fixed version ([WithVersion]), no negotiation is performed.
308-
//
309-
// If the API server's ping response does not contain an API version, or if the
310-
// client did not get a successful ping response, it assumes it is connected with
311-
// an old daemon that does not support API version negotiation, in which case it
312-
// downgrades to the lowest supported API version.
313-
func (cli *Client) NegotiateAPIVersion(ctx context.Context) {
314-
if !cli.manualOverride {
315-
// Avoid concurrent modification of version-related fields
316-
cli.negotiateLock.Lock()
317-
defer cli.negotiateLock.Unlock()
318-
319-
ping, err := cli.Ping(ctx, PingOptions{})
320-
if err != nil {
321-
// FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it.
322-
return
323-
}
324-
// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
325-
_ = cli.negotiateAPIVersion(ping.APIVersion)
326-
}
327-
}
328-
329-
// NegotiateAPIVersionPing downgrades the client's API version to match the
330-
// APIVersion in the ping response. If the API version in pingResponse is higher
331-
// than the maximum version supported by the client, it uses the client's maximum
332-
// version.
333-
//
334-
// If a manual override is in place, either through the "DOCKER_API_VERSION"
335-
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
336-
// with a fixed version ([WithVersion]), no negotiation is performed.
337-
//
338-
// If the API server's ping response does not contain an API version, it falls
339-
// back to the oldest API version supported.
340-
func (cli *Client) NegotiateAPIVersionPing(pingResponse PingResult) {
341-
// TODO(thaJeztah): should this take a "Ping" option? It only consumes the version. This method should be removed overall and not be exported.
342-
if !cli.manualOverride {
343-
// Avoid concurrent modification of version-related fields
344-
cli.negotiateLock.Lock()
345-
defer cli.negotiateLock.Unlock()
346-
347-
// FIXME(thaJeztah): we should not swallow the error here, and instead returning it.
348-
_ = cli.negotiateAPIVersion(pingResponse.APIVersion)
349-
}
350-
}
351-
352289
// negotiateAPIVersion updates the version to match the API version from
353290
// the ping response. It falls back to the lowest version supported if the
354291
// API version is empty, or returns an error if the API version is lower than

client/client_interfaces.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ type stableAPIClient interface {
3232
ClientVersion() string
3333
DaemonHost() string
3434
ServerVersion(ctx context.Context) (types.Version, error)
35-
NegotiateAPIVersion(ctx context.Context)
36-
NegotiateAPIVersionPing(PingResult)
3735
HijackDialer
3836
Dialer() func(context.Context) (net.Conn, error)
3937
Close() error

client/client_test.go

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,27 @@ func TestNewClientWithOpsFromEnvSetsDefaultVersion(t *testing.T) {
252252
func TestNegotiateAPIVersionEmpty(t *testing.T) {
253253
t.Setenv("DOCKER_API_VERSION", "")
254254

255-
client, err := NewClientWithOpts(FromEnv)
256-
assert.NilError(t, err)
257-
258-
// set our version to something new
259-
client.version = "1.51"
260-
261255
// if no version from server, expect the earliest
262256
// version before APIVersion was implemented
263257
const expected = fallbackAPIVersion
264258

259+
client, err := NewClientWithOpts(FromEnv,
260+
WithAPIVersionNegotiation(),
261+
WithMockClient(mockResponse(http.StatusOK, http.Header{"Api-Version": []string{expected}}, "OK")),
262+
)
263+
assert.NilError(t, err)
264+
265+
// set our version to something new.
266+
// we're not using [WithVersion] here, as that marks the version
267+
// as manually overridden.
268+
client.version = "1.51"
269+
265270
// test downgrade
266-
client.NegotiateAPIVersionPing(PingResult{})
271+
ping, err := client.Ping(t.Context(), PingOptions{
272+
NegotiateAPIVersion: true,
273+
})
274+
assert.NilError(t, err)
275+
assert.Check(t, is.Equal(ping.APIVersion, expected))
267276
assert.Check(t, is.Equal(client.ClientVersion(), expected))
268277
}
269278

@@ -275,6 +284,7 @@ func TestNegotiateAPIVersion(t *testing.T) {
275284
clientVersion string
276285
pingVersion string
277286
expectedVersion string
287+
expectedErr string
278288
}{
279289
{
280290
// client should downgrade to the version reported by the daemon.
@@ -304,6 +314,7 @@ func TestNegotiateAPIVersion(t *testing.T) {
304314
doc: "no downgrade old",
305315
pingVersion: "1.19",
306316
expectedVersion: MaxAPIVersion,
317+
expectedErr: "API version 1.19 is not supported by this client: the minimum supported API version is " + fallbackAPIVersion,
307318
},
308319
{
309320
// client should not upgrade to a newer version if a version was set,
@@ -317,7 +328,12 @@ func TestNegotiateAPIVersion(t *testing.T) {
317328

318329
for _, tc := range tests {
319330
t.Run(tc.doc, func(t *testing.T) {
320-
opts := make([]Opt, 0)
331+
opts := []Opt{
332+
FromEnv,
333+
WithAPIVersionNegotiation(),
334+
WithMockClient(mockResponse(http.StatusOK, http.Header{"Api-Version": []string{tc.pingVersion}}, "OK")),
335+
}
336+
321337
if tc.clientVersion != "" {
322338
// Note that this check is redundant, as WithVersion() considers
323339
// an empty version equivalent to "not setting a version", but
@@ -326,7 +342,14 @@ func TestNegotiateAPIVersion(t *testing.T) {
326342
}
327343
client, err := NewClientWithOpts(opts...)
328344
assert.NilError(t, err)
329-
client.NegotiateAPIVersionPing(PingResult{APIVersion: tc.pingVersion})
345+
_, err = client.Ping(t.Context(), PingOptions{
346+
NegotiateAPIVersion: true,
347+
})
348+
if tc.expectedErr != "" {
349+
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
350+
} else {
351+
assert.NilError(t, err)
352+
}
330353
assert.Check(t, is.Equal(tc.expectedVersion, client.ClientVersion()))
331354
})
332355
}
@@ -338,11 +361,16 @@ func TestNegotiateAPIVersionOverride(t *testing.T) {
338361
const expected = "9.99"
339362
t.Setenv("DOCKER_API_VERSION", expected)
340363

341-
client, err := NewClientWithOpts(FromEnv)
364+
client, err := NewClientWithOpts(
365+
FromEnv,
366+
WithMockClient(mockResponse(http.StatusOK, http.Header{"Api-Version": []string{"1.45"}}, "OK")),
367+
)
342368
assert.NilError(t, err)
343369

344370
// test that we honored the env var
345-
client.NegotiateAPIVersionPing(PingResult{APIVersion: "1.24"})
371+
_, err = client.Ping(t.Context(), PingOptions{
372+
NegotiateAPIVersion: true,
373+
})
346374
assert.Check(t, is.Equal(client.ClientVersion(), expected))
347375
}
348376

@@ -353,9 +381,10 @@ func TestNegotiateAPIVersionConnectionFailure(t *testing.T) {
353381

354382
client, err := NewClientWithOpts(WithHost("tcp://no-such-host.invalid"))
355383
assert.NilError(t, err)
356-
357384
client.version = expected
358-
client.NegotiateAPIVersion(context.Background())
385+
_, err = client.Ping(t.Context(), PingOptions{
386+
NegotiateAPIVersion: true,
387+
})
359388
assert.Check(t, is.Equal(client.ClientVersion(), expected))
360389
}
361390

@@ -392,22 +421,34 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) {
392421
// TestNegotiateAPIVersionWithEmptyVersion asserts that initializing a client
393422
// with an empty version string does still allow API-version negotiation
394423
func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
395-
client, err := NewClientWithOpts(WithVersion(""))
424+
client, err := NewClientWithOpts(
425+
WithVersion(""),
426+
WithMockClient(mockResponse(http.StatusOK, http.Header{"Api-Version": []string{"1.50"}}, "OK")),
427+
)
396428
assert.NilError(t, err)
397429

398430
const expected = "1.50"
399-
client.NegotiateAPIVersionPing(PingResult{APIVersion: expected})
431+
_, err = client.Ping(t.Context(), PingOptions{
432+
NegotiateAPIVersion: true,
433+
})
400434
assert.Check(t, is.Equal(client.ClientVersion(), expected))
401435
}
402436

403437
// TestNegotiateAPIVersionWithFixedVersion asserts that initializing a client
404438
// with a fixed version disables API-version negotiation
405439
func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) {
406440
const customVersion = "1.50"
407-
client, err := NewClientWithOpts(WithVersion(customVersion))
441+
client, err := NewClientWithOpts(
442+
WithVersion(customVersion),
443+
WithMockClient(mockResponse(http.StatusOK, http.Header{"Api-Version": []string{"1.49"}}, "OK")),
444+
)
408445
assert.NilError(t, err)
409446

410-
client.NegotiateAPIVersionPing(PingResult{APIVersion: "1.49"})
447+
_, err = client.Ping(t.Context(), PingOptions{
448+
NegotiateAPIVersion: true,
449+
ForceNegotiate: true,
450+
})
451+
assert.NilError(t, err)
411452
assert.Check(t, is.Equal(client.ClientVersion(), customVersion))
412453
}
413454

client/ping.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,29 @@ import (
1212

1313
// PingOptions holds options for [client.Ping].
1414
type PingOptions struct {
15-
// Add future optional parameters here
15+
// NegotiateAPIVersion queries the API and updates the version to match the API
16+
// version. NegotiateAPIVersion downgrades the client's API version to match the
17+
// APIVersion if the ping version is lower than the default version. If the API
18+
// version reported by the server is higher than the maximum version supported
19+
// by the client, it uses the client's maximum version.
20+
//
21+
// If a manual override is in place, either through the "DOCKER_API_VERSION"
22+
// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized
23+
// with a fixed version ([WithVersion]), no negotiation is performed.
24+
//
25+
// If the API server's ping response does not contain an API version, or if the
26+
// client did not get a successful ping response, it assumes it is connected with
27+
// an old daemon that does not support API version negotiation, in which case it
28+
// downgrades to the lowest supported API version.
29+
NegotiateAPIVersion bool
30+
31+
// ForceNegotiate forces the client to re-negotiate the API version, even if
32+
// API-version negotiation already happened. This option cannot be
33+
// used if the client is configured with a fixed version using (using
34+
// [WithVersion] or [WithVersionFromEnv]).
35+
//
36+
// This option has no effect if NegotiateAPIVersion is not set.
37+
ForceNegotiate bool
1638
}
1739

1840
// PingResult holds the result of a [Client.Ping] API call.
@@ -50,6 +72,30 @@ type SwarmStatus struct {
5072
// for other non-success status codes, failing to connect to the API, or failing
5173
// to parse the API response.
5274
func (cli *Client) Ping(ctx context.Context, options PingOptions) (PingResult, error) {
75+
if cli.manualOverride {
76+
return cli.ping(ctx)
77+
}
78+
if !options.NegotiateAPIVersion && !cli.negotiateVersion {
79+
return cli.ping(ctx)
80+
}
81+
82+
// Ensure exclusive write access to version and negotiated fields
83+
cli.negotiateLock.Lock()
84+
defer cli.negotiateLock.Unlock()
85+
86+
ping, err := cli.ping(ctx)
87+
if err != nil {
88+
return cli.ping(ctx)
89+
}
90+
91+
if cli.negotiated.Load() && !options.ForceNegotiate {
92+
return ping, nil
93+
}
94+
95+
return ping, cli.negotiateAPIVersion(ping.APIVersion)
96+
}
97+
98+
func (cli *Client) ping(ctx context.Context) (PingResult, error) {
5399
// Using cli.buildRequest() + cli.doRequest() instead of cli.sendRequest()
54100
// because ping requests are used during API version negotiation, so we want
55101
// to hit the non-versioned /_ping endpoint, not /v1.xx/_ping

0 commit comments

Comments
 (0)