Merge pull request #51233 from austinvazquez/refactor-client-version

client: refactor ServerVersion to return ServerVersionResult
This commit is contained in:
Sebastiaan van Stijn
2025-10-30 17:59:05 +01:00
committed by GitHub
10 changed files with 156 additions and 56 deletions

View File

@@ -5,7 +5,6 @@ import (
"io" "io"
"net" "net"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
) )
@@ -27,7 +26,7 @@ type stableAPIClient interface {
VolumeAPIClient VolumeAPIClient
ClientVersion() string ClientVersion() string
DaemonHost() string DaemonHost() string
ServerVersion(ctx context.Context) (types.Version, error) ServerVersion(ctx context.Context, options ServerVersionOptions) (ServerVersionResult, error)
HijackDialer HijackDialer
Dialer() func(context.Context) (net.Conn, error) Dialer() func(context.Context) (net.Conn, error)
Close() error Close() error

View File

@@ -7,15 +7,56 @@ import (
"github.com/moby/moby/api/types" "github.com/moby/moby/api/types"
) )
// ServerVersion returns information of the docker client and server host. // ServerVersionOptions specifies options for the server version request.
func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) { type ServerVersionOptions struct {
// Currently no options are supported.
}
// ServerVersionResult contains information about the Docker server host.
type ServerVersionResult struct {
// Platform is the platform (product name) the server is running on.
Platform PlatformInfo
// APIVersion is the highest API version supported by the server.
APIVersion string
// MinAPIVersion is the minimum API version the server supports.
MinAPIVersion string
// Components contains version information for the components making
// up the server. Information in this field is for informational
// purposes, and not part of the API contract.
Components []types.ComponentVersion
}
// PlatformInfo holds information about the platform (product name) the
// server is running on.
type PlatformInfo struct {
// Name is the name of the platform (for example, "Docker Engine - Community",
// or "Docker Desktop 4.49.0 (208003)")
Name string
}
// ServerVersion returns information of the Docker server host.
func (cli *Client) ServerVersion(ctx context.Context, _ ServerVersionOptions) (ServerVersionResult, error) {
resp, err := cli.get(ctx, "/version", nil, nil) resp, err := cli.get(ctx, "/version", nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return types.Version{}, err return ServerVersionResult{}, err
} }
var server types.Version var v types.Version
err = json.NewDecoder(resp.Body).Decode(&server) err = json.NewDecoder(resp.Body).Decode(&v)
return server, err if err != nil {
return ServerVersionResult{}, err
}
return ServerVersionResult{
Platform: PlatformInfo{
Name: v.Platform.Name,
},
APIVersion: v.APIVersion,
MinAPIVersion: v.MinAPIVersion,
Components: v.Components,
}, nil
} }

View File

@@ -43,11 +43,11 @@ func OnlyDefaultNetworks(ctx context.Context) bool {
} }
func IsAmd64() bool { func IsAmd64() bool {
return testEnv.DaemonVersion.Arch == "amd64" return testEnv.DaemonInfo.Architecture == "amd64"
} }
func NotPpc64le() bool { func NotPpc64le() bool {
return testEnv.DaemonVersion.Arch != "ppc64le" return testEnv.DaemonInfo.Architecture != "ppc64le"
} }
func UnixCli() bool { func UnixCli() bool {

View File

@@ -953,7 +953,7 @@ func TestEmptyPortBindingsBC(t *testing.T) {
// Skip this subtest if the daemon doesn't support the client version. // Skip this subtest if the daemon doesn't support the client version.
// TODO(aker): drop this once the Engine supports API version >= 1.53 // TODO(aker): drop this once the Engine supports API version >= 1.53
_, err := apiClient.ServerVersion(ctx) _, err := apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
if err != nil && strings.Contains(err.Error(), fmt.Sprintf("client version %s is too new", version)) { if err != nil && strings.Contains(err.Error(), fmt.Sprintf("client version %s is too new", version)) {
t.Skipf("requires API %s", version) t.Skipf("requires API %s", version)
} }

View File

@@ -106,7 +106,7 @@ func TestAuthZPluginAllowRequest(t *testing.T) {
assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID)) assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
_, err := c.ServerVersion(ctx) _, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, 1, ctrl.versionReqCount) assert.Equal(t, 1, ctrl.versionReqCount)
assert.Equal(t, 1, ctrl.versionResCount) assert.Equal(t, 1, ctrl.versionResCount)
@@ -137,7 +137,7 @@ func TestAuthZPluginTLS(t *testing.T) {
c, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath) c, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
assert.NilError(t, err) assert.NilError(t, err)
_, err = c.ServerVersion(ctx) _, err = c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "client", ctrl.reqUser) assert.Equal(t, "client", ctrl.reqUser)
@@ -165,7 +165,7 @@ func TestAuthZPluginDenyRequest(t *testing.T) {
c := d.NewClientT(t) c := d.NewClientT(t)
// Ensure command is blocked // Ensure command is blocked
_, err := c.ServerVersion(ctx) _, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.Assert(t, err != nil) assert.Assert(t, err != nil)
assert.Equal(t, 1, ctrl.versionReqCount) assert.Equal(t, 1, ctrl.versionReqCount)
assert.Equal(t, 0, ctrl.versionResCount) assert.Equal(t, 0, ctrl.versionResCount)
@@ -211,7 +211,7 @@ func TestAuthZPluginDenyResponse(t *testing.T) {
c := d.NewClientT(t) c := d.NewClientT(t)
// Ensure command is blocked // Ensure command is blocked
_, err := c.ServerVersion(ctx) _, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.Assert(t, err != nil) assert.Assert(t, err != nil)
assert.Equal(t, 1, ctrl.versionReqCount) assert.Equal(t, 1, ctrl.versionReqCount)
assert.Equal(t, 1, ctrl.versionResCount) assert.Equal(t, 1, ctrl.versionResCount)
@@ -304,7 +304,7 @@ func TestAuthZPluginErrorResponse(t *testing.T) {
c := d.NewClientT(t) c := d.NewClientT(t)
// Ensure command is blocked // Ensure command is blocked
_, err := c.ServerVersion(ctx) _, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.Assert(t, err != nil) assert.Assert(t, err != nil)
assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error()) assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
} }
@@ -317,7 +317,7 @@ func TestAuthZPluginErrorRequest(t *testing.T) {
c := d.NewClientT(t) c := d.NewClientT(t)
// Ensure command is blocked // Ensure command is blocked
_, err := c.ServerVersion(ctx) _, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.Assert(t, err != nil) assert.Assert(t, err != nil)
assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error()) assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
} }
@@ -331,7 +331,7 @@ func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
c := d.NewClientT(t) c := d.NewClientT(t)
_, err := c.ServerVersion(ctx) _, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
assert.NilError(t, err) assert.NilError(t, err)
// assert plugin is only called once.. // assert plugin is only called once..

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/moby/moby/api/types"
"github.com/moby/moby/client" "github.com/moby/moby/client"
"github.com/moby/moby/v2/internal/testutil/request" "github.com/moby/moby/v2/internal/testutil/request"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@@ -16,26 +17,45 @@ func TestVersion(t *testing.T) {
ctx := setupTest(t) ctx := setupTest(t)
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
version, err := apiClient.ServerVersion(ctx) version, err := apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, len(version.Components) > 0, "expected at least one component in version.Components")
assert.Check(t, version.APIVersion != "") var engine types.ComponentVersion
assert.Check(t, version.Version != "") var found bool
assert.Check(t, version.MinAPIVersion != "")
assert.Check(t, is.Equal(testEnv.DaemonInfo.ExperimentalBuild, version.Experimental)) for _, comp := range version.Components {
assert.Check(t, is.Equal(testEnv.DaemonInfo.OSType, version.Os)) if comp.Name == "Engine" {
engine = comp
found = true
break
}
}
assert.Check(t, found, "Engine component not found in version.Components")
assert.Equal(t, engine.Name, "Engine")
assert.Check(t, engine.Version != "")
assert.Equal(t, engine.Details["ApiVersion"], version.APIVersion)
assert.Equal(t, engine.Details["MinAPIVersion"], version.MinAPIVersion)
assert.Check(t, is.Equal(testEnv.DaemonInfo.OSType, engine.Details["Os"]))
experimentalStr := engine.Details["Experimental"]
experimentalBool, err := strconv.ParseBool(experimentalStr)
assert.NilError(t, err, "Experimental field in Engine details is not a valid boolean string")
assert.Equal(t, testEnv.DaemonInfo.ExperimentalBuild, experimentalBool)
} }
func TestAPIClientVersionOldNotSupported(t *testing.T) { func TestAPIClientVersionOldNotSupported(t *testing.T) {
ctx := setupTest(t) ctx := setupTest(t)
major, minor, _ := strings.Cut(testEnv.DaemonVersion.MinAPIVersion, ".") minApiVersion := testEnv.DaemonMinAPIVersion
major, minor, _ := strings.Cut(minApiVersion, ".")
vMinInt, err := strconv.Atoi(minor) vMinInt, err := strconv.Atoi(minor)
assert.NilError(t, err) assert.NilError(t, err)
vMinInt-- vMinInt--
version := fmt.Sprintf("%s.%d", major, vMinInt) version := fmt.Sprintf("%s.%d", major, vMinInt)
apiClient := request.NewAPIClient(t, client.WithVersion(version)) apiClient := request.NewAPIClient(t, client.WithVersion(version))
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) 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)
_, err = apiClient.ServerVersion(ctx) _, err = apiClient.ServerVersion(ctx, client.ServerVersionOptions{})
assert.Error(t, err, expectedErrorMessage) assert.Error(t, err, expectedErrorMessage)
} }

View File

@@ -9,7 +9,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
"github.com/moby/moby/client" "github.com/moby/moby/client"
"github.com/moby/moby/v2/internal/testutil/fixtures/load" "github.com/moby/moby/v2/internal/testutil/fixtures/load"
@@ -20,11 +19,11 @@ import (
// Execution contains information about the current test execution and daemon // Execution contains information about the current test execution and daemon
// under test // under test
type Execution struct { type Execution struct {
client client.APIClient client client.APIClient
DaemonInfo system.Info DaemonInfo system.Info
DaemonVersion types.Version DaemonMinAPIVersion string
PlatformDefaults PlatformDefaults PlatformDefaults PlatformDefaults
protectedElements protectedElements protectedElements protectedElements
} }
// PlatformDefaults are defaults values for the platform of the daemon under test // PlatformDefaults are defaults values for the platform of the daemon under test
@@ -46,22 +45,27 @@ func New(ctx context.Context) (*Execution, error) {
// FromClient creates a new Execution environment from the passed in client // FromClient creates a new Execution environment from the passed in client
func FromClient(ctx context.Context, c *client.Client) (*Execution, error) { func FromClient(ctx context.Context, c *client.Client) (*Execution, error) {
_, err := c.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true})
if err != nil {
return nil, errors.Wrapf(err, "failed to ping daemon to negotiate api version")
}
result, err := c.Info(ctx, client.InfoOptions{}) result, err := c.Info(ctx, client.InfoOptions{})
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get info from daemon") return nil, errors.Wrapf(err, "failed to get info from daemon")
} }
info := result.Info
v, err := c.ServerVersion(context.Background()) version, err := c.ServerVersion(ctx, client.ServerVersionOptions{})
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get version info from daemon") return nil, errors.Wrapf(err, "failed to get version from daemon")
} }
return &Execution{ return &Execution{
client: c, client: c,
DaemonInfo: info, DaemonInfo: result.Info,
DaemonVersion: v, DaemonMinAPIVersion: version.MinAPIVersion,
PlatformDefaults: getPlatformDefaults(info), PlatformDefaults: getPlatformDefaults(result.Info),
protectedElements: newProtectedElements(), protectedElements: newProtectedElements(),
}, nil }, nil
} }
@@ -129,11 +133,7 @@ func (e *Execution) IsRemoteDaemon() bool {
// DaemonAPIVersion returns the negotiated daemon api version // DaemonAPIVersion returns the negotiated daemon api version
func (e *Execution) DaemonAPIVersion() string { func (e *Execution) DaemonAPIVersion() string {
version, err := e.APIClient().ServerVersion(context.TODO()) return e.APIClient().ClientVersion()
if err != nil {
return ""
}
return version.APIVersion
} }
// Print the execution details to stdout // Print the execution details to stdout
@@ -228,7 +228,7 @@ func (e *Execution) GitHubActions() bool {
// NotAmd64 returns true if the daemon's architecture is not amd64 // NotAmd64 returns true if the daemon's architecture is not amd64
func (e *Execution) NotAmd64() bool { func (e *Execution) NotAmd64() bool {
return e.DaemonVersion.Arch != "amd64" return e.DaemonInfo.Architecture != "amd64"
} }
// FirewallBackendDriver returns the value of FirewallBackend.Driver from // FirewallBackendDriver returns the value of FirewallBackend.Driver from

View File

@@ -31,7 +31,7 @@ func ensureHTTPServerImage(t testing.TB) {
if goos == "" { if goos == "" {
goos = "linux" goos = "linux"
} }
goarch := testEnv.DaemonVersion.Arch goarch := testEnv.DaemonInfo.Architecture
if goarch == "" { if goarch == "" {
goarch = "amd64" goarch = "amd64"
} }

View File

@@ -5,7 +5,6 @@ import (
"io" "io"
"net" "net"
"github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
) )
@@ -27,7 +26,7 @@ type stableAPIClient interface {
VolumeAPIClient VolumeAPIClient
ClientVersion() string ClientVersion() string
DaemonHost() string DaemonHost() string
ServerVersion(ctx context.Context) (types.Version, error) ServerVersion(ctx context.Context, options ServerVersionOptions) (ServerVersionResult, error)
HijackDialer HijackDialer
Dialer() func(context.Context) (net.Conn, error) Dialer() func(context.Context) (net.Conn, error)
Close() error Close() error

View File

@@ -7,15 +7,56 @@ import (
"github.com/moby/moby/api/types" "github.com/moby/moby/api/types"
) )
// ServerVersion returns information of the docker client and server host. // ServerVersionOptions specifies options for the server version request.
func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) { type ServerVersionOptions struct {
// Currently no options are supported.
}
// ServerVersionResult contains information about the Docker server host.
type ServerVersionResult struct {
// Platform is the platform (product name) the server is running on.
Platform PlatformInfo
// APIVersion is the highest API version supported by the server.
APIVersion string
// MinAPIVersion is the minimum API version the server supports.
MinAPIVersion string
// Components contains version information for the components making
// up the server. Information in this field is for informational
// purposes, and not part of the API contract.
Components []types.ComponentVersion
}
// PlatformInfo holds information about the platform (product name) the
// server is running on.
type PlatformInfo struct {
// Name is the name of the platform (for example, "Docker Engine - Community",
// or "Docker Desktop 4.49.0 (208003)")
Name string
}
// ServerVersion returns information of the Docker server host.
func (cli *Client) ServerVersion(ctx context.Context, _ ServerVersionOptions) (ServerVersionResult, error) {
resp, err := cli.get(ctx, "/version", nil, nil) resp, err := cli.get(ctx, "/version", nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return types.Version{}, err return ServerVersionResult{}, err
} }
var server types.Version var v types.Version
err = json.NewDecoder(resp.Body).Decode(&server) err = json.NewDecoder(resp.Body).Decode(&v)
return server, err if err != nil {
return ServerVersionResult{}, err
}
return ServerVersionResult{
Platform: PlatformInfo{
Name: v.Platform.Name,
},
APIVersion: v.APIVersion,
MinAPIVersion: v.MinAPIVersion,
Components: v.Components,
}, nil
} }