diff --git a/cli/command/system/info.go b/cli/command/system/info.go index c19946d09e..452ba21644 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "io" + "io/ioutil" + "regexp" "sort" "strings" @@ -64,13 +66,6 @@ func NewInfoCommand(dockerCli command.Cli) *cobra.Command { func runInfo(cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error { var info info - ctx := context.Background() - if dinfo, err := dockerCli.Client().Info(ctx); err == nil { - info.Info = &dinfo - } else { - info.ServerErrors = append(info.ServerErrors, err.Error()) - } - info.ClientInfo = &clientInfo{ Context: dockerCli.CurrentContext(), Debug: debug.IsEnabled(), @@ -81,12 +76,60 @@ func runInfo(cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error info.ClientErrors = append(info.ClientErrors, err.Error()) } + if needsServerInfo(opts.format, info) { + ctx := context.Background() + if dinfo, err := dockerCli.Client().Info(ctx); err == nil { + info.Info = &dinfo + } else { + info.ServerErrors = append(info.ServerErrors, err.Error()) + } + } + if opts.format == "" { return prettyPrintInfo(dockerCli, info) } return formatInfo(dockerCli, info, opts.format) } +// placeHolders does a rudimentary match for possible placeholders in a +// template, matching a '.', followed by an letter (a-z/A-Z). +var placeHolders = regexp.MustCompile(`\.[a-zA-Z]`) + +// needsServerInfo detects if the given template uses any server information. +// If only client-side information is used in the template, we can skip +// connecting to the daemon. This allows (e.g.) to only get cli-plugin +// information, without also making a (potentially expensive) API call. +func needsServerInfo(template string, info info) bool { + if len(template) == 0 || placeHolders.FindString(template) == "" { + // The template is empty, or does not contain formatting fields + // (e.g. `table` or `raw` or `{{ json .}}`). Assume we need server-side + // information to render it. + return true + } + + // A template is provided and has at least one field set. + tmpl, err := templates.NewParse("", template) + if err != nil { + // ignore parsing errors here, and let regular code handle them + return true + } + + type sparseInfo struct { + ClientInfo *clientInfo `json:",omitempty"` + ClientErrors []string `json:",omitempty"` + } + + // This constructs an "info" object that only has the client-side fields. + err = tmpl.Execute(ioutil.Discard, sparseInfo{ + ClientInfo: info.ClientInfo, + ClientErrors: info.ClientErrors, + }) + // If executing the template failed, it means the template needs + // server-side information as well. If it succeeded without server-side + // information, we don't need to make API calls to collect that information. + return err != nil +} + func prettyPrintInfo(dockerCli command.Cli, info info) error { fmt.Fprintln(dockerCli.Out(), "Client:") if info.ClientInfo != nil { diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index d3762ea783..18453e9c26 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -420,3 +420,55 @@ func TestFormatInfo(t *testing.T) { }) } } + +func TestNeedsServerInfo(t *testing.T) { + tests := []struct { + doc string + template string + expected bool + }{ + { + doc: "no template", + template: "", + expected: true, + }, + { + doc: "JSON", + template: "json", + expected: true, + }, + { + doc: "JSON (all fields)", + template: "{{json .}}", + expected: true, + }, + { + doc: "JSON (Server ID)", + template: "{{json .ID}}", + expected: true, + }, + { + doc: "ClientInfo", + template: "{{json .ClientInfo}}", + expected: false, + }, + { + doc: "JSON ClientInfo", + template: "{{json .ClientInfo}}", + expected: false, + }, + { + doc: "JSON (Active context)", + template: "{{json .ClientInfo.Context}}", + expected: false, + }, + } + + inf := info{ClientInfo: &clientInfo{}} + for _, tc := range tests { + tc := tc + t.Run(tc.doc, func(t *testing.T) { + assert.Equal(t, needsServerInfo(tc.template, inf), tc.expected) + }) + } +}