Files
docker-cli/cli/command/system/version.go
Sebastiaan van Stijn bff56f0493 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>
2025-11-12 14:57:13 +01:00

260 lines
8.4 KiB
Go

package system
import (
"context"
"fmt"
"io"
"runtime"
"sort"
"strconv"
"text/template"
"time"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/formatter/tabwriter"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/version"
"github.com/docker/cli/templates"
"github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"github.com/tonistiigi/go-rosetta"
)
const defaultVersionTemplate = `{{with .Client -}}
Client:{{if ne .Platform nil}}{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}{{end}}
Version: {{.Version}}
API version: {{.APIVersion}}{{if ne .APIVersion .DefaultAPIVersion}} (downgraded from {{.DefaultAPIVersion}}){{end}}
Go version: {{.GoVersion}}
Git commit: {{.GitCommit}}
Built: {{.BuildTime}}
OS/Arch: {{.Os}}/{{.Arch}}
Context: {{.Context}}
{{- end}}
{{- if ne .Server nil}}{{with .Server}}
Server:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{end}}
{{- range $component := .Components}}
{{$component.Name}}:
{{- if eq $component.Name "Engine" }}
Version: {{.Version}}
API version: {{index .Details "ApiVersion"}} (minimum version {{index .Details "MinAPIVersion"}})
Go version: {{index .Details "GoVersion"}}
Git commit: {{index .Details "GitCommit"}}
Built: {{index .Details "BuildTime"}}
OS/Arch: {{index .Details "Os"}}/{{index .Details "Arch"}}
Experimental: {{index .Details "Experimental"}}
{{- else }}
Version: {{$component.Version}}
{{- $detailsOrder := getDetailsOrder $component}}
{{- range $key := $detailsOrder}}
{{$key}}: {{index $component.Details $key}}
{{- end}}
{{- end}}
{{- end}}
{{- end}}{{- end}}`
type versionOptions struct {
format string
}
// versionInfo contains version information of both the Client, and Server
type versionInfo struct {
Client clientVersion
Server *serverVersion
}
type platformInfo struct {
Name string `json:"Name,omitempty"`
}
type clientVersion struct {
Platform *platformInfo `json:"Platform,omitempty"`
Version string `json:"Version,omitempty"`
APIVersion string `json:"ApiVersion,omitempty"`
DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"`
GitCommit string `json:"GitCommit,omitempty"`
GoVersion string `json:"GoVersion,omitempty"`
Os string `json:"Os,omitempty"`
Arch string `json:"Arch,omitempty"`
BuildTime string `json:"BuildTime,omitempty"`
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
// information.
func newClientVersion(contextName string, dockerCli command.Cli) clientVersion {
v := clientVersion{
Version: version.Version,
DefaultAPIVersion: client.MaxAPIVersion,
GoVersion: runtime.Version(),
GitCommit: version.GitCommit,
BuildTime: reformatDate(version.BuildTime),
Os: runtime.GOOS,
Arch: arch(),
Context: contextName,
}
if version.PlatformName != "" {
v.Platform = &platformInfo{Name: version.PlatformName}
}
if dockerCli != nil {
v.APIVersion = dockerCli.CurrentVersion()
}
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
cmd := &cobra.Command{
Use: "version [OPTIONS]",
Short: "Show the Docker version information",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runVersion(cmd.Context(), dockerCLI, &opts)
},
Annotations: map[string]string{
"category-top": "10",
},
ValidArgsFunction: cobra.NoFileCompletions,
DisableFlagsInUseLine: true,
}
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
return cmd
}
func reformatDate(buildTime string) string {
t, errTime := time.Parse(time.RFC3339Nano, buildTime)
if errTime == nil {
return t.Format(time.ANSIC)
}
return buildTime
}
func arch() string {
out := runtime.GOARCH
if rosetta.Enabled() {
out += " (rosetta)"
}
return out
}
func runVersion(ctx context.Context, dockerCLI command.Cli, opts *versionOptions) error {
var err error
tmpl, err := newVersionTemplate(opts.format)
if err != nil {
return cli.StatusError{StatusCode: 64, Status: err.Error()}
}
vd := versionInfo{
Client: newClientVersion(dockerCLI.CurrentContext(), dockerCLI),
}
sv, err := dockerCLI.Client().ServerVersion(ctx, client.ServerVersionOptions{})
if err == nil {
vd.Server = newServerVersion(sv)
}
if err2 := prettyPrintVersion(dockerCLI.Out(), vd, tmpl); err2 != nil && err == nil {
err = err2
}
return err
}
func prettyPrintVersion(out io.Writer, vd versionInfo, tmpl *template.Template) error {
t := tabwriter.NewWriter(out, 20, 1, 1, ' ', 0)
err := tmpl.Execute(t, vd)
_, _ = t.Write([]byte("\n"))
_ = t.Flush()
return err
}
func newVersionTemplate(templateFormat string) (*template.Template, error) {
switch templateFormat {
case "":
templateFormat = defaultVersionTemplate
case formatter.JSONFormatKey:
templateFormat = formatter.JSONFormat
}
tmpl, err := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}).Parse(templateFormat)
if err != nil {
return nil, fmt.Errorf("template parsing error: %w", err)
}
return tmpl, nil
}
func getDetailsOrder(v system.ComponentVersion) []string {
out := make([]string, 0, len(v.Details))
for k := range v.Details {
out = append(out, k)
}
sort.Strings(out)
return out
}