mirror of https://github.com/docker/cli.git
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 <github@gone.nl>
This commit is contained in:
parent
d7a311ba74
commit
d738e7c489
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -64,13 +66,6 @@ func NewInfoCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
func runInfo(cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error {
|
func runInfo(cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error {
|
||||||
var info info
|
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{
|
info.ClientInfo = &clientInfo{
|
||||||
Context: dockerCli.CurrentContext(),
|
Context: dockerCli.CurrentContext(),
|
||||||
Debug: debug.IsEnabled(),
|
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())
|
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 == "" {
|
if opts.format == "" {
|
||||||
return prettyPrintInfo(dockerCli, info)
|
return prettyPrintInfo(dockerCli, info)
|
||||||
}
|
}
|
||||||
return formatInfo(dockerCli, info, opts.format)
|
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 {
|
func prettyPrintInfo(dockerCli command.Cli, info info) error {
|
||||||
fmt.Fprintln(dockerCli.Out(), "Client:")
|
fmt.Fprintln(dockerCli.Out(), "Client:")
|
||||||
if info.ClientInfo != nil {
|
if info.ClientInfo != nil {
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue