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:
Sebastiaan van Stijn 2021-07-06 14:43:42 +02:00
parent d7a311ba74
commit d738e7c489
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
2 changed files with 102 additions and 7 deletions

View File

@ -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 {

View File

@ -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)
})
}
}