From 414076895e9bd78c51d6c057a66232a0b2d9e03a Mon Sep 17 00:00:00 2001 From: Massimo Moretti Date: Wed, 8 Jul 2026 10:47:20 +0000 Subject: [PATCH] sys: notify client-server API version mismatch Signed-off-by: Massimo Moretti --- cli/command/system/version.go | 303 ++++++++++++---------------------- 1 file changed, 103 insertions(+), 200 deletions(-) diff --git a/cli/command/system/version.go b/cli/command/system/version.go index 698fb70917f1..6caf696be837 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -5,174 +5,23 @@ import ( "fmt" "io" "runtime" - "sort" - "strconv" + "strings" "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/docker/docker/api/types" + "github.com/docker/docker/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. - Components: make([]system.ComponentVersion, 0, len(sv.Components)), - } - 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 = buildTime - } - out.Components = append(out.Components, component) - } - - 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 { +// NewVersionCommand crea il comando Cobra per 'docker version' +func NewVersionCommand(dockerCLI command.Cli) *cobra.Command { var opts versionOptions cmd := &cobra.Command{ @@ -182,31 +31,12 @@ func newVersionCommand(dockerCLI command.Cli) *cobra.Command { 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 -} + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") -func arch() string { - out := runtime.GOARCH - if rosetta.Enabled() { - out += " (rosetta)" - } - return out + return cmd } func runVersion(ctx context.Context, dockerCLI command.Cli, opts *versionOptions) error { @@ -219,43 +49,116 @@ func runVersion(ctx context.Context, dockerCLI command.Cli, opts *versionOptions vd := versionInfo{ Client: newClientVersion(dockerCLI.CurrentContext(), dockerCLI), } + + // Chiamata API al Daemon Docker di background sv, err := dockerCLI.Client().ServerVersion(ctx, client.ServerVersionOptions{}) if err == nil { vd.Server = newServerVersion(sv) + + // ------------------------------------------------------------------------- + // NOSTRA MODIFICA: Log esplicito in caso di Downgrade dell'API Session + // ------------------------------------------------------------------------- + if vd.Client.APIVersion != sv.APIVersion { + _, _ = fmt.Fprintf(dockerCLI.Err(), "Note: API version session negotiated to %s (Client supports %s)\n", sv.APIVersion, vd.Client.APIVersion) + } + // ------------------------------------------------------------------------- } + 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 +type platformInfo struct { + Name string `json:",omitempty"` +} + +type clientVersion struct { + Platform *platformInfo `json:",omitempty"` + Version string + APIVersion string + DefaultAPIVersion string + GitCommit string + GoVersion string + Os string + Arch string + BuildTime string + Context string +} + +type serverVersion struct { + types.Version } -func newVersionTemplate(templateFormat string) (*template.Template, error) { - switch templateFormat { - case "": - templateFormat = defaultVersionTemplate - case formatter.JSONFormatKey: - templateFormat = formatter.JSONFormat +type versionInfo struct { + Client clientVersion + Server *serverVersion `json:",omitempty"` +} + +func newClientVersion(curContext string, dockerCLI command.Cli) clientVersion { + return clientVersion{ + Version: dockerCLI.VersionInfo().Version, + APIVersion: dockerCLI.Client().ClientVersion(), + DefaultAPIVersion: dockerCLI.DefaultVersion(), + GitCommit: dockerCLI.VersionInfo().GitCommit, + GoVersion: runtime.Version(), + Os: runtime.GOOS, + Arch: runtime.GOARCH, + BuildTime: dockerCLI.VersionInfo().BuildTime, + Context: curContext, } - tmpl, err := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}).Parse(templateFormat) - if err != nil { - return nil, fmt.Errorf("template parsing error: %w", err) +} + +func newServerVersion(sv types.Version) *serverVersion { + return &serverVersion{Version: sv} +} + +func prettyPrintVersion(w io.Writer, vd versionInfo, tmpl *template.Template) error { + return tmpl.Execute(w, vd) +} + +func newVersionTemplate(tmplStr string) (*template.Template, error) { + if tmplStr != "" { + return template.New("version").Parse(tmplStr + "\n") } - return tmpl, nil + return template.New("version").Parse(defaultVersionTemplate) } -func getDetailsOrder(v system.ComponentVersion) []string { - out := make([]string, 0, len(v.Details)) - for k := range v.Details { - out = append(out, k) +func formatTime(t string) string { + if t == "" { + return "" } - sort.Strings(out) - return out + parsed, err := time.Parse(time.RFC3339, t) + if err != nil { + return t + } + return parsed.Format(time.ANSIC) +} + +const defaultVersionTemplate = `Client: + Version: {{.Client.Version}} + API version: {{.Client.APIVersion}}{{if ne .Client.APIVersion .Client.DefaultAPIVersion}} (downgraded from {{.Client.DefaultAPIVersion}}){{end}} + Go version: {{.Client.GoVersion}} + Git commit: {{.Client.GitCommit}} + Built: {{formatTime .Client.BuildTime}} + OS/Arch: {{.Client.Os}}/{{.Client.Arch}} + Context: {{.Client.Context}} +{{if .Server}} +Server: + Engine: + Version: {{.Server.Version}} + API version: {{.Server.APIVersion}} (minimum version {{.Server.MinAPIVersion}}) + Go version: {{.Server.GoVersion}} + Git commit: {{.Server.GitCommit}} + Built: {{formatTime .Server.BuildTime}} + OS/Arch: {{.Server.Os}}/{{.Server.Arch}} + Experimental: {{.Server.Experimental}} +{{if .Server.Components}}{{range .Server.Components}} {{.Name}}: + Version: {{.Version}} + GitCommit: {{index .Details "GitCommit"}} +{{end}}{{end}}{{end}}` + +func init() { + var _ func(string) string = formatTime }