Browse code

client: refactor ServerVersion to return ServerVersionResult

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Austin Vazquez authored on 2025/10/21 04:04:38
Showing 10 changed files
... ...
@@ -5,7 +5,6 @@ import (
5 5
 	"io"
6 6
 	"net"
7 7
 
8
-	"github.com/moby/moby/api/types"
9 8
 	"github.com/moby/moby/api/types/network"
10 9
 	"github.com/moby/moby/api/types/system"
11 10
 )
... ...
@@ -27,7 +26,7 @@ type stableAPIClient interface {
27 27
 	VolumeAPIClient
28 28
 	ClientVersion() string
29 29
 	DaemonHost() string
30
-	ServerVersion(ctx context.Context) (types.Version, error)
30
+	ServerVersion(ctx context.Context, options ServerVersionOptions) (ServerVersionResult, error)
31 31
 	HijackDialer
32 32
 	Dialer() func(context.Context) (net.Conn, error)
33 33
 	Close() error
... ...
@@ -7,15 +7,56 @@ import (
7 7
 	"github.com/moby/moby/api/types"
8 8
 )
9 9
 
10
-// ServerVersion returns information of the docker client and server host.
11
-func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) {
10
+// ServerVersionOptions specifies options for the server version request.
11
+type ServerVersionOptions struct {
12
+	// Currently no options are supported.
13
+}
14
+
15
+// ServerVersionResult contains information about the Docker server host.
16
+type ServerVersionResult struct {
17
+	// Platform is the platform (product name) the server is running on.
18
+	Platform PlatformInfo
19
+
20
+	// APIVersion is the highest API version supported by the server.
21
+	APIVersion string
22
+
23
+	// MinAPIVersion is the minimum API version the server supports.
24
+	MinAPIVersion string
25
+
26
+	// Components contains version information for the components making
27
+	// up the server. Information in this field is for informational
28
+	// purposes, and not part of the API contract.
29
+	Components []types.ComponentVersion
30
+}
31
+
32
+// PlatformInfo holds information about the platform (product name) the
33
+// server is running on.
34
+type PlatformInfo struct {
35
+	// Name is the name of the platform (for example, "Docker Engine - Community",
36
+	// or "Docker Desktop 4.49.0 (208003)")
37
+	Name string
38
+}
39
+
40
+// ServerVersion returns information of the Docker server host.
41
+func (cli *Client) ServerVersion(ctx context.Context, _ ServerVersionOptions) (ServerVersionResult, error) {
12 42
 	resp, err := cli.get(ctx, "/version", nil, nil)
13 43
 	defer ensureReaderClosed(resp)
14 44
 	if err != nil {
15
-		return types.Version{}, err
45
+		return ServerVersionResult{}, err
46
+	}
47
+
48
+	var v types.Version
49
+	err = json.NewDecoder(resp.Body).Decode(&v)
50
+	if err != nil {
51
+		return ServerVersionResult{}, err
16 52
 	}
17 53
 
18
-	var server types.Version
19
-	err = json.NewDecoder(resp.Body).Decode(&server)
20
-	return server, err
54
+	return ServerVersionResult{
55
+		Platform: PlatformInfo{
56
+			Name: v.Platform.Name,
57
+		},
58
+		APIVersion:    v.APIVersion,
59
+		MinAPIVersion: v.MinAPIVersion,
60
+		Components:    v.Components,
61
+	}, nil
21 62
 }
... ...
@@ -43,11 +43,11 @@ func OnlyDefaultNetworks(ctx context.Context) bool {
43 43
 }
44 44
 
45 45
 func IsAmd64() bool {
46
-	return testEnv.DaemonVersion.Arch == "amd64"
46
+	return testEnv.DaemonInfo.Architecture == "amd64"
47 47
 }
48 48
 
49 49
 func NotPpc64le() bool {
50
-	return testEnv.DaemonVersion.Arch != "ppc64le"
50
+	return testEnv.DaemonInfo.Architecture != "ppc64le"
51 51
 }
52 52
 
53 53
 func UnixCli() bool {
... ...
@@ -953,7 +953,7 @@ func TestEmptyPortBindingsBC(t *testing.T) {
953 953
 
954 954
 		// Skip this subtest if the daemon doesn't support the client version.
955 955
 		// TODO(aker): drop this once the Engine supports API version >= 1.53
956
-		_, err := apiClient.ServerVersion(ctx)
956
+		_, err := apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
957 957
 		if err != nil && strings.Contains(err.Error(), fmt.Sprintf("client version %s is too new", version)) {
958 958
 			t.Skipf("requires API %s", version)
959 959
 		}
... ...
@@ -106,7 +106,7 @@ func TestAuthZPluginAllowRequest(t *testing.T) {
106 106
 	assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
107 107
 	assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
108 108
 
109
-	_, err := c.ServerVersion(ctx)
109
+	_, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
110 110
 	assert.NilError(t, err)
111 111
 	assert.Equal(t, 1, ctrl.versionReqCount)
112 112
 	assert.Equal(t, 1, ctrl.versionResCount)
... ...
@@ -137,7 +137,7 @@ func TestAuthZPluginTLS(t *testing.T) {
137 137
 	c, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
138 138
 	assert.NilError(t, err)
139 139
 
140
-	_, err = c.ServerVersion(ctx)
140
+	_, err = c.ServerVersion(ctx, client.ServerVersionOptions{})
141 141
 	assert.NilError(t, err)
142 142
 
143 143
 	assert.Equal(t, "client", ctrl.reqUser)
... ...
@@ -165,7 +165,7 @@ func TestAuthZPluginDenyRequest(t *testing.T) {
165 165
 	c := d.NewClientT(t)
166 166
 
167 167
 	// Ensure command is blocked
168
-	_, err := c.ServerVersion(ctx)
168
+	_, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
169 169
 	assert.Assert(t, err != nil)
170 170
 	assert.Equal(t, 1, ctrl.versionReqCount)
171 171
 	assert.Equal(t, 0, ctrl.versionResCount)
... ...
@@ -211,7 +211,7 @@ func TestAuthZPluginDenyResponse(t *testing.T) {
211 211
 	c := d.NewClientT(t)
212 212
 
213 213
 	// Ensure command is blocked
214
-	_, err := c.ServerVersion(ctx)
214
+	_, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
215 215
 	assert.Assert(t, err != nil)
216 216
 	assert.Equal(t, 1, ctrl.versionReqCount)
217 217
 	assert.Equal(t, 1, ctrl.versionResCount)
... ...
@@ -304,7 +304,7 @@ func TestAuthZPluginErrorResponse(t *testing.T) {
304 304
 	c := d.NewClientT(t)
305 305
 
306 306
 	// Ensure command is blocked
307
-	_, err := c.ServerVersion(ctx)
307
+	_, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
308 308
 	assert.Assert(t, err != nil)
309 309
 	assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
310 310
 }
... ...
@@ -317,7 +317,7 @@ func TestAuthZPluginErrorRequest(t *testing.T) {
317 317
 	c := d.NewClientT(t)
318 318
 
319 319
 	// Ensure command is blocked
320
-	_, err := c.ServerVersion(ctx)
320
+	_, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
321 321
 	assert.Assert(t, err != nil)
322 322
 	assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
323 323
 }
... ...
@@ -331,7 +331,7 @@ func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
331 331
 
332 332
 	c := d.NewClientT(t)
333 333
 
334
-	_, err := c.ServerVersion(ctx)
334
+	_, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
335 335
 	assert.NilError(t, err)
336 336
 
337 337
 	// assert plugin is only called once..
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"strings"
7 7
 	"testing"
8 8
 
9
+	"github.com/moby/moby/api/types"
9 10
 	"github.com/moby/moby/client"
10 11
 	"github.com/moby/moby/v2/internal/testutil/request"
11 12
 	"gotest.tools/v3/assert"
... ...
@@ -16,26 +17,45 @@ func TestVersion(t *testing.T) {
16 16
 	ctx := setupTest(t)
17 17
 	apiClient := testEnv.APIClient()
18 18
 
19
-	version, err := apiClient.ServerVersion(ctx)
19
+	version, err := apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
20 20
 	assert.NilError(t, err)
21
+	assert.Check(t, len(version.Components) > 0, "expected at least one component in version.Components")
21 22
 
22
-	assert.Check(t, version.APIVersion != "")
23
-	assert.Check(t, version.Version != "")
24
-	assert.Check(t, version.MinAPIVersion != "")
25
-	assert.Check(t, is.Equal(testEnv.DaemonInfo.ExperimentalBuild, version.Experimental))
26
-	assert.Check(t, is.Equal(testEnv.DaemonInfo.OSType, version.Os))
23
+	var engine types.ComponentVersion
24
+	var found bool
25
+
26
+	for _, comp := range version.Components {
27
+		if comp.Name == "Engine" {
28
+			engine = comp
29
+			found = true
30
+			break
31
+		}
32
+	}
33
+
34
+	assert.Check(t, found, "Engine component not found in version.Components")
35
+	assert.Equal(t, engine.Name, "Engine")
36
+	assert.Check(t, engine.Version != "")
37
+	assert.Equal(t, engine.Details["ApiVersion"], version.APIVersion)
38
+	assert.Equal(t, engine.Details["MinAPIVersion"], version.MinAPIVersion)
39
+	assert.Check(t, is.Equal(testEnv.DaemonInfo.OSType, engine.Details["Os"]))
40
+
41
+	experimentalStr := engine.Details["Experimental"]
42
+	experimentalBool, err := strconv.ParseBool(experimentalStr)
43
+	assert.NilError(t, err, "Experimental field in Engine details is not a valid boolean string")
44
+	assert.Equal(t, testEnv.DaemonInfo.ExperimentalBuild, experimentalBool)
27 45
 }
28 46
 
29 47
 func TestAPIClientVersionOldNotSupported(t *testing.T) {
30 48
 	ctx := setupTest(t)
31
-	major, minor, _ := strings.Cut(testEnv.DaemonVersion.MinAPIVersion, ".")
49
+	minApiVersion := testEnv.DaemonMinAPIVersion
50
+	major, minor, _ := strings.Cut(minApiVersion, ".")
32 51
 	vMinInt, err := strconv.Atoi(minor)
33 52
 	assert.NilError(t, err)
34 53
 	vMinInt--
35 54
 	version := fmt.Sprintf("%s.%d", major, vMinInt)
36 55
 	apiClient := request.NewAPIClient(t, client.WithVersion(version))
37 56
 
38
-	expectedErrorMessage := fmt.Sprintf("Error response from daemon: client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", version, testEnv.DaemonVersion.MinAPIVersion)
39
-	_, err = apiClient.ServerVersion(ctx)
57
+	expectedErrorMessage := fmt.Sprintf("Error response from daemon: client version %s is too old. Minimum supported API version is %s, please upgrade your client to a newer version", version, minApiVersion)
58
+	_, err = apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
40 59
 	assert.Error(t, err, expectedErrorMessage)
41 60
 }
... ...
@@ -9,7 +9,6 @@ import (
9 9
 	"strings"
10 10
 	"testing"
11 11
 
12
-	"github.com/moby/moby/api/types"
13 12
 	"github.com/moby/moby/api/types/system"
14 13
 	"github.com/moby/moby/client"
15 14
 	"github.com/moby/moby/v2/internal/testutil/fixtures/load"
... ...
@@ -20,11 +19,11 @@ import (
20 20
 // Execution contains information about the current test execution and daemon
21 21
 // under test
22 22
 type Execution struct {
23
-	client            client.APIClient
24
-	DaemonInfo        system.Info
25
-	DaemonVersion     types.Version
26
-	PlatformDefaults  PlatformDefaults
27
-	protectedElements protectedElements
23
+	client              client.APIClient
24
+	DaemonInfo          system.Info
25
+	DaemonMinAPIVersion string
26
+	PlatformDefaults    PlatformDefaults
27
+	protectedElements   protectedElements
28 28
 }
29 29
 
30 30
 // PlatformDefaults are defaults values for the platform of the daemon under test
... ...
@@ -46,22 +45,27 @@ func New(ctx context.Context) (*Execution, error) {
46 46
 
47 47
 // FromClient creates a new Execution environment from the passed in client
48 48
 func FromClient(ctx context.Context, c *client.Client) (*Execution, error) {
49
+	_, err := c.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true})
50
+	if err != nil {
51
+		return nil, errors.Wrapf(err, "failed to ping daemon to negotiate api version")
52
+	}
53
+
49 54
 	result, err := c.Info(ctx, client.InfoOptions{})
50 55
 	if err != nil {
51 56
 		return nil, errors.Wrapf(err, "failed to get info from daemon")
52 57
 	}
53
-	info := result.Info
54
-	v, err := c.ServerVersion(context.Background())
58
+
59
+	version, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
55 60
 	if err != nil {
56
-		return nil, errors.Wrapf(err, "failed to get version info from daemon")
61
+		return nil, errors.Wrapf(err, "failed to get version from daemon")
57 62
 	}
58 63
 
59 64
 	return &Execution{
60
-		client:            c,
61
-		DaemonInfo:        info,
62
-		DaemonVersion:     v,
63
-		PlatformDefaults:  getPlatformDefaults(info),
64
-		protectedElements: newProtectedElements(),
65
+		client:              c,
66
+		DaemonInfo:          result.Info,
67
+		DaemonMinAPIVersion: version.MinAPIVersion,
68
+		PlatformDefaults:    getPlatformDefaults(result.Info),
69
+		protectedElements:   newProtectedElements(),
65 70
 	}, nil
66 71
 }
67 72
 
... ...
@@ -129,11 +133,7 @@ func (e *Execution) IsRemoteDaemon() bool {
129 129
 
130 130
 // DaemonAPIVersion returns the negotiated daemon api version
131 131
 func (e *Execution) DaemonAPIVersion() string {
132
-	version, err := e.APIClient().ServerVersion(context.TODO())
133
-	if err != nil {
134
-		return ""
135
-	}
136
-	return version.APIVersion
132
+	return e.APIClient().ClientVersion()
137 133
 }
138 134
 
139 135
 // Print the execution details to stdout
... ...
@@ -228,7 +228,7 @@ func (e *Execution) GitHubActions() bool {
228 228
 
229 229
 // NotAmd64 returns true if the daemon's architecture is not amd64
230 230
 func (e *Execution) NotAmd64() bool {
231
-	return e.DaemonVersion.Arch != "amd64"
231
+	return e.DaemonInfo.Architecture != "amd64"
232 232
 }
233 233
 
234 234
 // FirewallBackendDriver returns the value of FirewallBackend.Driver from
... ...
@@ -31,7 +31,7 @@ func ensureHTTPServerImage(t testing.TB) {
31 31
 	if goos == "" {
32 32
 		goos = "linux"
33 33
 	}
34
-	goarch := testEnv.DaemonVersion.Arch
34
+	goarch := testEnv.DaemonInfo.Architecture
35 35
 	if goarch == "" {
36 36
 		goarch = "amd64"
37 37
 	}
... ...
@@ -5,7 +5,6 @@ import (
5 5
 	"io"
6 6
 	"net"
7 7
 
8
-	"github.com/moby/moby/api/types"
9 8
 	"github.com/moby/moby/api/types/network"
10 9
 	"github.com/moby/moby/api/types/system"
11 10
 )
... ...
@@ -27,7 +26,7 @@ type stableAPIClient interface {
27 27
 	VolumeAPIClient
28 28
 	ClientVersion() string
29 29
 	DaemonHost() string
30
-	ServerVersion(ctx context.Context) (types.Version, error)
30
+	ServerVersion(ctx context.Context, options ServerVersionOptions) (ServerVersionResult, error)
31 31
 	HijackDialer
32 32
 	Dialer() func(context.Context) (net.Conn, error)
33 33
 	Close() error
... ...
@@ -7,15 +7,56 @@ import (
7 7
 	"github.com/moby/moby/api/types"
8 8
 )
9 9
 
10
-// ServerVersion returns information of the docker client and server host.
11
-func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) {
10
+// ServerVersionOptions specifies options for the server version request.
11
+type ServerVersionOptions struct {
12
+	// Currently no options are supported.
13
+}
14
+
15
+// ServerVersionResult contains information about the Docker server host.
16
+type ServerVersionResult struct {
17
+	// Platform is the platform (product name) the server is running on.
18
+	Platform PlatformInfo
19
+
20
+	// APIVersion is the highest API version supported by the server.
21
+	APIVersion string
22
+
23
+	// MinAPIVersion is the minimum API version the server supports.
24
+	MinAPIVersion string
25
+
26
+	// Components contains version information for the components making
27
+	// up the server. Information in this field is for informational
28
+	// purposes, and not part of the API contract.
29
+	Components []types.ComponentVersion
30
+}
31
+
32
+// PlatformInfo holds information about the platform (product name) the
33
+// server is running on.
34
+type PlatformInfo struct {
35
+	// Name is the name of the platform (for example, "Docker Engine - Community",
36
+	// or "Docker Desktop 4.49.0 (208003)")
37
+	Name string
38
+}
39
+
40
+// ServerVersion returns information of the Docker server host.
41
+func (cli *Client) ServerVersion(ctx context.Context, _ ServerVersionOptions) (ServerVersionResult, error) {
12 42
 	resp, err := cli.get(ctx, "/version", nil, nil)
13 43
 	defer ensureReaderClosed(resp)
14 44
 	if err != nil {
15
-		return types.Version{}, err
45
+		return ServerVersionResult{}, err
46
+	}
47
+
48
+	var v types.Version
49
+	err = json.NewDecoder(resp.Body).Decode(&v)
50
+	if err != nil {
51
+		return ServerVersionResult{}, err
16 52
 	}
17 53
 
18
-	var server types.Version
19
-	err = json.NewDecoder(resp.Body).Decode(&server)
20
-	return server, err
54
+	return ServerVersionResult{
55
+		Platform: PlatformInfo{
56
+			Name: v.Platform.Name,
57
+		},
58
+		APIVersion:    v.APIVersion,
59
+		MinAPIVersion: v.MinAPIVersion,
60
+		Components:    v.Components,
61
+	}, nil
21 62
 }