cli/command/system: define struct for formatting version

The client.ServerVersion method in the moby/client module defines
an output struct that's separate from the API response. These output
structs are not designed to be marshaled as JSON, but the CLI depended
on them defining `json` labels, which it used to format the output
as JSON (`docker version --format=json`); as a result, the JSON output
changed in docker v29, as it would now use the naming based on the Go
struct's fields (`APIVersion` instead of `ApiVersion`).

In future, we should consider having a `--raw` (or similar) option for
the CLI to print API responses as-is, instead of using client structs
or CLI structs for this (this would also make sure the JSON output does
not inherit client-side formatting of fields).

For now, let's create a struct for formatting the output, similar to what
we do for the client-side information.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-11-12 13:57:50 +01:00
parent 3d4129b9ea
commit bff56f0493
4 changed files with 85 additions and 36 deletions

View File

@ -1 +1 @@
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"","APIVersion":"","MinAPIVersion":"","Os":"","Arch":"","Experimental":false,"Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}]}}
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"18.99.5-ce","ApiVersion":"1.30","MinAPIVersion":"1.12","Os":"linux","Arch":"amd64","Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"GitCommit":"64ddfa6","GoVersion":"go1.8.7","KernelVersion":"v1.0.0","BuildTime":"Mon Jul 9 23:38:38 2018"}}

View File

@ -1 +1 @@
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"","APIVersion":"","MinAPIVersion":"","Os":"","Arch":"","Experimental":false,"Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}]}}
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"18.99.5-ce","ApiVersion":"1.30","MinAPIVersion":"1.12","Os":"linux","Arch":"amd64","Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"GitCommit":"64ddfa6","GoVersion":"go1.8.7","KernelVersion":"v1.0.0","BuildTime":"Mon Jul 9 23:38:38 2018"}}

View File

@ -6,6 +6,7 @@ import (
"io"
"runtime"
"sort"
"strconv"
"text/template"
"time"
@ -63,7 +64,7 @@ type versionOptions struct {
// versionInfo contains version information of both the Client, and Server
type versionInfo struct {
Client clientVersion
Server *client.ServerVersionResult
Server *serverVersion
}
type platformInfo struct {
@ -83,6 +84,26 @@ type clientVersion struct {
Context string `json:"Context"`
}
// serverVersion contains information about the Docker server host.
// it's the client-side presentation of [client.ServerVersionResult].
type serverVersion struct {
Platform client.PlatformInfo `json:",omitempty"` // Platform is the platform (product name) the server is running on.
Version string `json:"Version"` // Version is the version of the daemon.
APIVersion string `json:"ApiVersion"` // APIVersion is the highest API version supported by the server.
MinAPIVersion string `json:"MinAPIVersion,omitempty"` // MinAPIVersion is the minimum API version the server supports.
Os string `json:"Os"` // Os is the operating system the server runs on.
Arch string `json:"Arch"` // Arch is the hardware architecture the server runs on.
Components []system.ComponentVersion `json:"Components,omitempty"` // Components contains version information for the components making up the server.
// The following fields are deprecated, they relate to the Engine component and are kept for backwards compatibility
GitCommit string `json:"GitCommit,omitempty"`
GoVersion string `json:"GoVersion,omitempty"`
KernelVersion string `json:"KernelVersion,omitempty"`
Experimental bool `json:"Experimental,omitempty"`
BuildTime string `json:"BuildTime,omitempty"`
}
// newClientVersion constructs a new clientVersion. If a dockerCLI is
// passed as argument, additional information is included (API version),
// which may invoke an API connection. Pass nil to omit the additional
@ -107,6 +128,47 @@ func newClientVersion(contextName string, dockerCli command.Cli) clientVersion {
return v
}
func newServerVersion(sv client.ServerVersionResult) *serverVersion {
out := &serverVersion{
Platform: sv.Platform,
Version: sv.Version,
APIVersion: sv.APIVersion,
MinAPIVersion: sv.MinAPIVersion,
Os: sv.Os,
Arch: sv.Arch,
Experimental: sv.Experimental, //nolint:staticcheck // ignore deprecated field.
}
foundEngine := false
for _, component := range sv.Components {
if component.Name == "Engine" {
foundEngine = true
buildTime, ok := component.Details["BuildTime"]
if ok {
component.Details["BuildTime"] = reformatDate(buildTime)
}
out.GitCommit = component.Details["GitCommit"]
out.GoVersion = component.Details["GoVersion"]
out.KernelVersion = component.Details["KernelVersion"]
out.Experimental = func() bool { b, _ := strconv.ParseBool(component.Details["Experimental"]); return b }()
out.BuildTime = reformatDate(component.Details["BuildTime"])
}
}
if !foundEngine {
out.Components = append(out.Components, system.ComponentVersion{
Name: "Engine",
Version: sv.Version,
Details: map[string]string{
"ApiVersion": sv.APIVersion,
"MinAPIVersion": sv.MinAPIVersion,
"Os": sv.Os,
"Arch": sv.Arch,
},
})
}
return out
}
// newVersionCommand creates a new cobra.Command for `docker version`
func newVersionCommand(dockerCLI command.Cli) *cobra.Command {
var opts versionOptions
@ -138,14 +200,14 @@ func reformatDate(buildTime string) string {
}
func arch() string {
arch := runtime.GOARCH
out := runtime.GOARCH
if rosetta.Enabled() {
arch += " (rosetta)"
out += " (rosetta)"
}
return arch
return out
}
func runVersion(ctx context.Context, dockerCli command.Cli, opts *versionOptions) error {
func runVersion(ctx context.Context, dockerCLI command.Cli, opts *versionOptions) error {
var err error
tmpl, err := newVersionTemplate(opts.format)
if err != nil {
@ -153,36 +215,13 @@ func runVersion(ctx context.Context, dockerCli command.Cli, opts *versionOptions
}
vd := versionInfo{
Client: newClientVersion(dockerCli.CurrentContext(), dockerCli),
Client: newClientVersion(dockerCLI.CurrentContext(), dockerCLI),
}
sv, err := dockerCli.Client().ServerVersion(ctx, client.ServerVersionOptions{})
sv, err := dockerCLI.Client().ServerVersion(ctx, client.ServerVersionOptions{})
if err == nil {
vd.Server = &sv
foundEngine := false
for _, component := range sv.Components {
if component.Name == "Engine" {
foundEngine = true
buildTime, ok := component.Details["BuildTime"]
if ok {
component.Details["BuildTime"] = reformatDate(buildTime)
}
}
}
if !foundEngine {
vd.Server.Components = append(vd.Server.Components, system.ComponentVersion{
Name: "Engine",
Version: sv.Version,
Details: map[string]string{
"ApiVersion": sv.APIVersion,
"MinAPIVersion": sv.MinAPIVersion,
"Os": sv.Os,
"Arch": sv.Arch,
},
})
}
vd.Server = newServerVersion(sv)
}
if err2 := prettyPrintVersion(dockerCli.Out(), vd, tmpl); err2 != nil && err == nil {
if err2 := prettyPrintVersion(dockerCLI.Out(), vd, tmpl); err2 != nil && err == nil {
err = err2
}
return err

View File

@ -47,8 +47,18 @@ func TestVersionFormat(t *testing.T) {
BuildTime: "Wed May 30 22:21:05 2018",
Context: "my-context",
},
Server: &client.ServerVersionResult{
Platform: client.PlatformInfo{Name: "Docker Enterprise Edition (EE) 2.0"},
Server: &serverVersion{
Platform: client.PlatformInfo{Name: "Docker Enterprise Edition (EE) 2.0"},
Version: "18.99.5-ce",
APIVersion: "1.30",
MinAPIVersion: "1.12",
Os: "linux",
Arch: "amd64",
GitCommit: "64ddfa6",
GoVersion: "go1.8.7",
KernelVersion: "v1.0.0",
Experimental: false,
BuildTime: "Mon Jul 9 23:38:38 2018",
Components: []system.ComponentVersion{
{
Name: "Engine",