From d738e7c4897602e5fa0e2d38a599348529242354 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 6 Jul 2021 14:43:42 +0200 Subject: [PATCH] docker info: skip API connection if possible The docker info output contains both "local" and "remote" (daemon-side) information. The API endpoint to collect daemon information (`/info`) is known to be "heavy", and (depending on what information is needed) not needed. This patch checks if the template (`--format`) used requires information from the daemon, and if not, omits making an API request. This will improve performance if (for example), the current "context" is requested from `docker info` or if only plugin information is requested. Before: time docker info --format '{{range .ClientInfo.Plugins}}Plugin: {{.Name}}, {{end}}' Plugin: buildx, Plugin: compose, Plugin: scan, ________________________________________________________ Executed in 301.91 millis fish external usr time 168.64 millis 82.00 micros 168.56 millis sys time 113.72 millis 811.00 micros 112.91 millis time docker info --format '{{json .ClientInfo.Plugins}}' time docker info --format '{{.ClientInfo.Context}}' default ________________________________________________________ Executed in 334.38 millis fish external usr time 177.23 millis 93.00 micros 177.13 millis sys time 124.90 millis 927.00 micros 123.97 millis docker context use remote-ssh-daemon time docker info --format '{{.ClientInfo.Context}}' remote-ssh-daemon ________________________________________________________ Executed in 1.22 secs fish external usr time 116.93 millis 110.00 micros 116.82 millis sys time 144.36 millis 887.00 micros 143.47 millis And daemon logs: Jul 06 12:42:12 remote-ssh-daemon dockerd[14377]: time="2021-07-06T12:42:12.139529947Z" level=debug msg="Calling HEAD /_ping" Jul 06 12:42:12 remote-ssh-daemon dockerd[14377]: time="2021-07-06T12:42:12.140772052Z" level=debug msg="Calling HEAD /_ping" Jul 06 12:42:12 remote-ssh-daemon dockerd[14377]: time="2021-07-06T12:42:12.163832016Z" level=debug msg="Calling GET /v1.41/info" After: time ./build/docker info --format '{{range .ClientInfo.Plugins}}Plugin: {{.Name}}, {{end}}' Plugin: buildx, Plugin: compose, Plugin: scan, ________________________________________________________ Executed in 139.84 millis fish external usr time 76.53 millis 62.00 micros 76.46 millis sys time 69.25 millis 723.00 micros 68.53 millis time ./build/docker info --format '{{.ClientInfo.Context}}' default ________________________________________________________ Executed in 136.94 millis fish external usr time 74.61 millis 74.00 micros 74.54 millis sys time 65.77 millis 858.00 micros 64.91 millis docker context use remote-ssh-daemon time ./build/docker info --format '{{.ClientInfo.Context}}' remote-ssh-daemon ________________________________________________________ Executed in 1.02 secs fish external usr time 74.25 millis 76.00 micros 74.17 millis sys time 65.09 millis 643.00 micros 64.44 millis And daemon logs: Jul 06 12:42:55 remote-ssh-daemon dockerd[14377]: time="2021-07-06T12:42:55.313654687Z" level=debug msg="Calling HEAD /_ping" Jul 06 12:42:55 remote-ssh-daemon dockerd[14377]: time="2021-07-06T12:42:55.314811624Z" level=debug msg="Calling HEAD /_ping" Signed-off-by: Sebastiaan van Stijn --- cli/command/system/info.go | 57 +++++++++++++++++++++++++++++---- cli/command/system/info_test.go | 52 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) 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) + }) + } +}