Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker info: skip API connection if possible #3179

Merged
merged 1 commit into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 50 additions & 7 deletions cli/command/system/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"regexp"
"sort"
"strings"

Expand Down Expand Up @@ -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(),
Expand All @@ -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 {
Expand Down
52 changes: 52 additions & 0 deletions cli/command/system/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}