package system import ( "context" "runtime" "sort" "strconv" "text/tabwriter" "text/template" "time" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" kubecontext "github.com/docker/cli/cli/context/kubernetes" "github.com/docker/cli/cli/version" "github.com/docker/cli/kubernetes" "github.com/docker/cli/templates" kubeapi "github.com/docker/compose-on-kubernetes/api" "github.com/docker/docker/api/types" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/tonistiigi/go-rosetta" kubernetesClient "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" ) var versionTemplate = `{{with .Client -}} Client:{{if ne .Platform.Name ""}} {{.Platform.Name}}{{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}} Experimental: {{.Experimental}} {{- end}} {{- if .ServerOK}}{{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 kubeConfig string } // versionInfo contains version information of both the Client, and Server type versionInfo struct { Client clientVersion Server *types.Version } type clientVersion struct { Platform struct{ Name string } `json:",omitempty"` Version string APIVersion string `json:"ApiVersion"` DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"` GitCommit string GoVersion string Os string Arch string BuildTime string `json:",omitempty"` Context string Experimental bool `json:",omitempty"` // Deprecated: experimental CLI features always enabled. This field is kept for backward-compatibility, and is always "true" } type kubernetesVersion struct { Kubernetes string StackAPI string } // ServerOK returns true when the client could connect to the docker server // and parse the information received. It returns false otherwise. func (v versionInfo) ServerOK() bool { return v.Server != nil } // 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(dockerCli, &opts) }, } flags := cmd.Flags() flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") flags.StringVar(&opts.kubeConfig, "kubeconfig", "", "Kubernetes config file") flags.SetAnnotation("kubeconfig", "kubernetes", nil) flags.SetAnnotation("kubeconfig", "deprecated", nil) 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 { arch := runtime.GOARCH if rosetta.Enabled() { arch += " (rosetta)" } return arch } func runVersion(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()} } orchestrator, err := dockerCli.StackOrchestrator("") if err != nil { return cli.StatusError{StatusCode: 64, Status: err.Error()} } vd := versionInfo{ Client: clientVersion{ Platform: struct{ Name string }{version.PlatformName}, Version: version.Version, APIVersion: dockerCli.Client().ClientVersion(), DefaultAPIVersion: dockerCli.DefaultVersion(), GoVersion: runtime.Version(), GitCommit: version.GitCommit, BuildTime: reformatDate(version.BuildTime), Os: runtime.GOOS, Arch: arch(), Experimental: true, Context: dockerCli.CurrentContext(), }, } sv, err := dockerCli.Client().ServerVersion(context.Background()) if err == nil { vd.Server = &sv var kubeVersion *kubernetesVersion if orchestrator.HasKubernetes() { kubeVersion = getKubernetesVersion(dockerCli, opts.kubeConfig) } foundEngine := false foundKubernetes := false for _, component := range sv.Components { switch component.Name { case "Engine": foundEngine = true buildTime, ok := component.Details["BuildTime"] if ok { component.Details["BuildTime"] = reformatDate(buildTime) } case "Kubernetes": foundKubernetes = true if _, ok := component.Details["StackAPI"]; !ok && kubeVersion != nil { component.Details["StackAPI"] = kubeVersion.StackAPI } } } if !foundEngine { vd.Server.Components = append(vd.Server.Components, types.ComponentVersion{ Name: "Engine", Version: sv.Version, Details: map[string]string{ "ApiVersion": sv.APIVersion, "MinAPIVersion": sv.MinAPIVersion, "GitCommit": sv.GitCommit, "GoVersion": sv.GoVersion, "Os": sv.Os, "Arch": sv.Arch, "BuildTime": reformatDate(vd.Server.BuildTime), "Experimental": strconv.FormatBool(sv.Experimental), }, }) } if !foundKubernetes && kubeVersion != nil { vd.Server.Components = append(vd.Server.Components, types.ComponentVersion{ Name: "Kubernetes", Version: kubeVersion.Kubernetes, Details: map[string]string{ "StackAPI": kubeVersion.StackAPI, }, }) } } if err2 := prettyPrintVersion(dockerCli, vd, tmpl); err2 != nil && err == nil { err = err2 } return err } func prettyPrintVersion(dockerCli command.Cli, vd versionInfo, tmpl *template.Template) error { t := tabwriter.NewWriter(dockerCli.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) { if templateFormat == "" { templateFormat = versionTemplate } tmpl := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}) tmpl, err := tmpl.Parse(templateFormat) return tmpl, errors.Wrap(err, "template parsing error") } func getDetailsOrder(v types.ComponentVersion) []string { out := make([]string, 0, len(v.Details)) for k := range v.Details { out = append(out, k) } sort.Strings(out) return out } func getKubernetesVersion(dockerCli command.Cli, kubeConfig string) *kubernetesVersion { version := kubernetesVersion{ Kubernetes: "Unknown", StackAPI: "Unknown", } var ( clientConfig clientcmd.ClientConfig err error ) if dockerCli.CurrentContext() == "" { clientConfig = kubeapi.NewKubernetesConfig(kubeConfig) } else { clientConfig, err = kubecontext.ConfigFromContext(dockerCli.CurrentContext(), dockerCli.ContextStore()) } if err != nil { logrus.Debugf("failed to get Kubernetes configuration: %s", err) return &version } config, err := clientConfig.ClientConfig() if err != nil { logrus.Debugf("failed to get Kubernetes client config: %s", err) return &version } kubeClient, err := kubernetesClient.NewForConfig(config) if err != nil { logrus.Debugf("failed to get Kubernetes client: %s", err) return &version } version.StackAPI = getStackVersion(kubeClient) version.Kubernetes = getKubernetesServerVersion(kubeClient) return &version } func getStackVersion(client *kubernetesClient.Clientset) string { apiVersion, err := kubernetes.GetStackAPIVersion(client) if err != nil { logrus.Debugf("failed to get Stack API version: %s", err) return "Unknown" } return string(apiVersion) } func getKubernetesServerVersion(client *kubernetesClient.Clientset) string { kubeVersion, err := client.DiscoveryClient.ServerVersion() if err != nil { logrus.Debugf("failed to get Kubernetes server version: %s", err) return "Unknown" } return kubeVersion.String() }