DockerCLI/cli/command/system/info.go

525 lines
18 KiB
Go
Raw Normal View History

add //go:build directives to prevent downgrading to go1.16 language This is a follow-up to 0e73168b7e6d1d029d76d05b843b1aaec46739a8 This repository is not yet a module (i.e., does not have a `go.mod`). This is not problematic when building the code in GOPATH or "vendor" mode, but when using the code as a module-dependency (in module-mode), different semantics are applied since Go1.21, which switches Go _language versions_ on a per-module, per-package, or even per-file base. A condensed summary of that logic [is as follows][1]: - For modules that have a go.mod containing a go version directive; that version is considered a minimum _required_ version (starting with the go1.19.13 and go1.20.8 patch releases: before those, it was only a recommendation). - For dependencies that don't have a go.mod (not a module), go language version go1.16 is assumed. - Likewise, for modules that have a go.mod, but the file does not have a go version directive, go language version go1.16 is assumed. - If a go.work file is present, but does not have a go version directive, language version go1.17 is assumed. When switching language versions, Go _downgrades_ the language version, which means that language features (such as generics, and `any`) are not available, and compilation fails. For example: # github.com/docker/cli/cli/context/store /go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod) /go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod) Note that these fallbacks are per-module, per-package, and can even be per-file, so _(indirect) dependencies_ can still use modern language features, as long as their respective go.mod has a version specified. Unfortunately, these failures do not occur when building locally (using vendor / GOPATH mode), but will affect consumers of the module. Obviously, this situation is not ideal, and the ultimate solution is to move to go modules (add a go.mod), but this comes with a non-insignificant risk in other areas (due to our complex dependency tree). We can revert to using go1.16 language features only, but this may be limiting, and may still be problematic when (e.g.) matching signatures of dependencies. There is an escape hatch: adding a `//go:build` directive to files that make use of go language features. From the [go toolchain docs][2]: > The go line for each module sets the language version the compiler enforces > when compiling packages in that module. The language version can be changed > on a per-file basis by using a build constraint. > > For example, a module containing code that uses the Go 1.21 language version > should have a `go.mod` file with a go line such as `go 1.21` or `go 1.21.3`. > If a specific source file should be compiled only when using a newer Go > toolchain, adding `//go:build go1.22` to that source file both ensures that > only Go 1.22 and newer toolchains will compile the file and also changes > the language version in that file to Go 1.22. This patch adds `//go:build` directives to those files using recent additions to the language. It's currently using go1.19 as version to match the version in our "vendor.mod", but we can consider being more permissive ("any" requires go1.18 or up), or more "optimistic" (force go1.21, which is the version we currently use to build). For completeness sake, note that any file _without_ a `//go:build` directive will continue to use go1.16 language version when used as a module. [1]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L9-L56 [2]; https://go.dev/doc/toolchain#:~:text=The%20go%20line%20for,file%20to%20Go%201.22 Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-12-14 07:51:57 -05:00
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.19
package system
import (
"context"
"fmt"
"io"
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>
2021-07-06 08:43:42 -04:00
"regexp"
"sort"
"strings"
"github.com/docker/cli/cli"
pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/debug"
flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/templates"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/registry"
"github.com/docker/go-units"
"github.com/spf13/cobra"
)
type infoOptions struct {
format string
}
type clientInfo struct {
Debug bool
clientVersion
Plugins []pluginmanager.Plugin
Warnings []string
}
type dockerInfo struct {
// This field should/could be ServerInfo but is anonymous to
// preserve backwards compatibility in the JSON rendering
// which has ServerInfo immediately within the top-level
// object.
*system.Info `json:",omitempty"`
ServerErrors []string `json:",omitempty"`
UserName string `json:"-"`
ClientInfo *clientInfo `json:",omitempty"`
ClientErrors []string `json:",omitempty"`
}
func (i *dockerInfo) clientPlatform() string {
if i.ClientInfo != nil && i.ClientInfo.Platform != nil {
return i.ClientInfo.Platform.Name
}
return ""
}
// NewInfoCommand creates a new cobra.Command for `docker info`
func NewInfoCommand(dockerCli command.Cli) *cobra.Command {
var opts infoOptions
cmd := &cobra.Command{
Use: "info [OPTIONS]",
Short: "Display system-wide information",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runInfo(cmd.Context(), cmd, dockerCli, &opts)
},
Annotations: map[string]string{
"category-top": "12",
"aliases": "docker system info, docker info",
},
ValidArgsFunction: completion.NoComplete,
}
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
return cmd
}
func runInfo(ctx context.Context, cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error {
info := dockerInfo{
ClientInfo: &clientInfo{
// Don't pass a dockerCLI to newClientVersion(), because we currently
// don't include negotiated API version, and want to avoid making an
// API connection when only printing the Client section.
clientVersion: newClientVersion(dockerCli.CurrentContext(), nil),
Debug: debug.IsEnabled(),
},
Info: &system.Info{},
}
if plugins, err := pluginmanager.ListPlugins(dockerCli, cmd.Root()); err == nil {
info.ClientInfo.Plugins = plugins
} else {
info.ClientErrors = append(info.ClientErrors, err.Error())
}
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>
2021-07-06 08:43:42 -04:00
if needsServerInfo(opts.format, info) {
if dinfo, err := dockerCli.Client().Info(ctx); err == nil {
info.Info = &dinfo
} else {
info.ServerErrors = append(info.ServerErrors, err.Error())
info: don't print server info if we failed to connect Before this patch, the Server output would be printed even if we failed to connect (including WARNINGS): ```bash docker -H tcp://127.0.0.1:2375 info Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? Client: Context: default Debug Mode: false Plugins: buildx: Docker Buildx (Docker Inc., v0.8.2) compose: Docker Compose (Docker Inc., v2.4.1) sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc., 0.6.0) scan: Docker Scan (Docker Inc., v0.17.0) Server: Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 Plugins: Volume: Network: Log: Swarm: NodeID: Is Manager: false Node Address: CPUs: 0 Total Memory: 0B Docker Root Dir: Debug Mode: false Experimental: false Live Restore Enabled: false WARNING: No memory limit support WARNING: No swap limit support WARNING: No oom kill disable support WARNING: No cpu cfs quota support WARNING: No cpu cfs period support WARNING: No cpu shares support WARNING: No cpuset support WARNING: IPv4 forwarding is disabled WARNING: bridge-nf-call-iptables is disabled WARNING: bridge-nf-call-ip6tables is disabled ERROR: Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? errors pretty printing info ``` With this patch; ```bash docker -H tcp://127.0.0.1:2375 info Client: Context: default Debug Mode: false Plugins: buildx: Docker Buildx (Docker Inc., v0.8.2) compose: Docker Compose (Docker Inc., v2.4.1) sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc., 0.6.0) scan: Docker Scan (Docker Inc., v0.17.0) Server: ERROR: Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? errors pretty printing info ``` And if a custom format is used: ```bash docker -H tcp://127.0.0.1:2375 info --format '{{.Containers}}' Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? 0 ``` Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-05-10 06:13:03 -04:00
if opts.format == "" {
// reset the server info to prevent printing "empty" Server info
// and warnings, but don't reset it if a custom format was specified
// to prevent errors from Go's template parsing during format.
info.Info = nil
} else {
// if a format is provided, print the error, as it may be hidden
// otherwise if the template doesn't include the ServerErrors field.
fprintln(dockerCli.Err(), err)
info: don't print server info if we failed to connect Before this patch, the Server output would be printed even if we failed to connect (including WARNINGS): ```bash docker -H tcp://127.0.0.1:2375 info Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? Client: Context: default Debug Mode: false Plugins: buildx: Docker Buildx (Docker Inc., v0.8.2) compose: Docker Compose (Docker Inc., v2.4.1) sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc., 0.6.0) scan: Docker Scan (Docker Inc., v0.17.0) Server: Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 Plugins: Volume: Network: Log: Swarm: NodeID: Is Manager: false Node Address: CPUs: 0 Total Memory: 0B Docker Root Dir: Debug Mode: false Experimental: false Live Restore Enabled: false WARNING: No memory limit support WARNING: No swap limit support WARNING: No oom kill disable support WARNING: No cpu cfs quota support WARNING: No cpu cfs period support WARNING: No cpu shares support WARNING: No cpuset support WARNING: IPv4 forwarding is disabled WARNING: bridge-nf-call-iptables is disabled WARNING: bridge-nf-call-ip6tables is disabled ERROR: Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? errors pretty printing info ``` With this patch; ```bash docker -H tcp://127.0.0.1:2375 info Client: Context: default Debug Mode: false Plugins: buildx: Docker Buildx (Docker Inc., v0.8.2) compose: Docker Compose (Docker Inc., v2.4.1) sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc., 0.6.0) scan: Docker Scan (Docker Inc., v0.17.0) Server: ERROR: Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? errors pretty printing info ``` And if a custom format is used: ```bash docker -H tcp://127.0.0.1:2375 info --format '{{.Containers}}' Cannot connect to the Docker daemon at tcp://127.0.0.1:2375. Is the docker daemon running? 0 ``` Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-05-10 06:13:03 -04:00
}
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>
2021-07-06 08:43:42 -04:00
}
}
if opts.format == "" {
info.UserName = dockerCli.ConfigFile().AuthConfigs[registry.IndexServer].Username
info.ClientInfo.APIVersion = dockerCli.CurrentVersion()
return prettyPrintInfo(dockerCli, info)
}
return formatInfo(dockerCli.Out(), info, opts.format)
}
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>
2021-07-06 08:43:42 -04:00
// 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 dockerInfo) bool {
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>
2021-07-06 08:43:42 -04:00
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(io.Discard, sparseInfo{
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>
2021-07-06 08:43:42 -04:00
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(streams command.Streams, info dockerInfo) error {
// Only append the platform info if it's not empty, to prevent printing a trailing space.
if p := info.clientPlatform(); p != "" {
fprintln(streams.Out(), "Client:", p)
} else {
fprintln(streams.Out(), "Client:")
}
if info.ClientInfo != nil {
prettyPrintClientInfo(streams, *info.ClientInfo)
}
for _, err := range info.ClientErrors {
fprintln(streams.Err(), "ERROR:", err)
}
fprintln(streams.Out())
fprintln(streams.Out(), "Server:")
if info.Info != nil {
for _, err := range prettyPrintServerInfo(streams, &info) {
info.ServerErrors = append(info.ServerErrors, err.Error())
}
}
for _, err := range info.ServerErrors {
fprintln(streams.Err(), "ERROR:", err)
}
if len(info.ServerErrors) > 0 || len(info.ClientErrors) > 0 {
return fmt.Errorf("errors pretty printing info")
}
return nil
}
func prettyPrintClientInfo(streams command.Streams, info clientInfo) {
fprintlnNonEmpty(streams.Out(), " Version: ", info.Version)
fprintln(streams.Out(), " Context: ", info.Context)
fprintln(streams.Out(), " Debug Mode:", info.Debug)
if len(info.Plugins) > 0 {
fprintln(streams.Out(), " Plugins:")
for _, p := range info.Plugins {
if p.Err == nil {
fprintf(streams.Out(), " %s: %s (%s)\n", p.Name, p.ShortDescription, p.Vendor)
fprintlnNonEmpty(streams.Out(), " Version: ", p.Version)
fprintlnNonEmpty(streams.Out(), " Path: ", p.Path)
} else {
info.Warnings = append(info.Warnings, fmt.Sprintf("WARNING: Plugin %q is not valid: %s", p.Path, p.Err))
}
}
}
if len(info.Warnings) > 0 {
fprintln(streams.Err(), strings.Join(info.Warnings, "\n"))
}
}
//nolint:gocyclo
func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
var errs []error
output := streams.Out()
fprintln(output, " Containers:", info.Containers)
fprintln(output, " Running:", info.ContainersRunning)
fprintln(output, " Paused:", info.ContainersPaused)
fprintln(output, " Stopped:", info.ContainersStopped)
fprintln(output, " Images:", info.Images)
fprintlnNonEmpty(output, " Server Version:", info.ServerVersion)
fprintlnNonEmpty(output, " Storage Driver:", info.Driver)
if info.DriverStatus != nil {
for _, pair := range info.DriverStatus {
fprintf(output, " %s: %s\n", pair[0], pair[1])
}
}
if info.SystemStatus != nil {
for _, pair := range info.SystemStatus {
fprintf(output, " %s: %s\n", pair[0], pair[1])
}
}
fprintlnNonEmpty(output, " Logging Driver:", info.LoggingDriver)
fprintlnNonEmpty(output, " Cgroup Driver:", info.CgroupDriver)
fprintlnNonEmpty(output, " Cgroup Version:", info.CgroupVersion)
fprintln(output, " Plugins:")
fprintln(output, " Volume:", strings.Join(info.Plugins.Volume, " "))
fprintln(output, " Network:", strings.Join(info.Plugins.Network, " "))
if len(info.Plugins.Authorization) != 0 {
fprintln(output, " Authorization:", strings.Join(info.Plugins.Authorization, " "))
}
fprintln(output, " Log:", strings.Join(info.Plugins.Log, " "))
if len(info.CDISpecDirs) > 0 {
fprintln(output, " CDI spec directories:")
for _, dir := range info.CDISpecDirs {
fprintf(output, " %s\n", dir)
}
}
fprintln(output, " Swarm:", info.Swarm.LocalNodeState)
printSwarmInfo(output, *info.Info)
if len(info.Runtimes) > 0 {
names := make([]string, 0, len(info.Runtimes))
for name := range info.Runtimes {
names = append(names, name)
}
fprintln(output, " Runtimes:", strings.Join(names, " "))
fprintln(output, " Default Runtime:", info.DefaultRuntime)
}
if info.OSType == "linux" {
fprintln(output, " Init Binary:", info.InitBinary)
for _, ci := range []struct {
Name string
Commit system.Commit
}{
{"containerd", info.ContainerdCommit},
{"runc", info.RuncCommit},
{"init", info.InitCommit},
} {
fprintf(output, " %s version: %s", ci.Name, ci.Commit.ID)
if ci.Commit.ID != ci.Commit.Expected {
fprintf(output, " (expected: %s)", ci.Commit.Expected)
}
fprintln(output)
}
if len(info.SecurityOptions) != 0 {
if kvs, err := system.DecodeSecurityOptions(info.SecurityOptions); err != nil {
errs = append(errs, err)
} else {
fprintln(output, " Security Options:")
for _, so := range kvs {
fprintln(output, " "+so.Name)
for _, o := range so.Options {
linting: address else/if/elseif statements found by gocritic cli/command/formatter/tabwriter/tabwriter.go:579:10: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli/connhelper/connhelper.go:43:2: singleCaseSwitch: should rewrite switch statement to if statement (gocritic) switch scheme := u.Scheme; scheme { ^ cli/compose/loader/loader.go:666:10: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ opts/hosts_test.go:173:10: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli-plugins/manager/candidate_test.go:78:4: ifElseChain: rewrite if-else to switch statement (gocritic) if tc.err != "" { ^ cli/command/checkpoint/formatter.go:15:2: singleCaseSwitch: should rewrite switch statement to if statement (gocritic) switch source { ^ cli/command/image/formatter_history.go:25:2: singleCaseSwitch: should rewrite switch statement to if statement (gocritic) switch source { ^ cli/command/service/scale.go:107:2: ifElseChain: rewrite if-else to switch statement (gocritic) if serviceMode.Replicated != nil { ^ cli/command/service/update.go:804:9: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli/command/service/update.go:222:2: ifElseChain: rewrite if-else to switch statement (gocritic) if sendAuth { ^ cli/command/container/formatter_diff.go:17:2: singleCaseSwitch: should rewrite switch statement to if statement (gocritic) switch source { ^ cli/command/container/start.go:79:2: ifElseChain: rewrite if-else to switch statement (gocritic) if opts.Attach || opts.OpenStdin { ^ cli/command/container/utils.go:84:11: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli/command/container/exec_test.go:200:11: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli/command/container/logs_test.go:52:11: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli/command/container/opts_test.go:1014:10: elseif: can replace 'else {if cond {}}' with 'else if cond {}' (gocritic) } else { ^ cli/command/system/info.go:297:7: singleCaseSwitch: should rewrite switch statement to if statement (gocritic) switch o.Key { ^ cli/command/system/version.go:164:4: singleCaseSwitch: should rewrite switch statement to if statement (gocritic) switch component.Name { ^ cli/command/system/info_test.go:478:4: ifElseChain: rewrite if-else to switch statement (gocritic) if tc.expectedOut != "" { ^ Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-11-20 07:54:53 -05:00
if o.Key == "profile" {
fprintln(output, " Profile:", o.Value)
}
}
}
}
}
}
// Isolation only has meaning on a Windows daemon.
if info.OSType == "windows" {
fprintln(output, " Default Isolation:", info.Isolation)
}
fprintlnNonEmpty(output, " Kernel Version:", info.KernelVersion)
fprintlnNonEmpty(output, " Operating System:", info.OperatingSystem)
fprintlnNonEmpty(output, " OSType:", info.OSType)
fprintlnNonEmpty(output, " Architecture:", info.Architecture)
fprintln(output, " CPUs:", info.NCPU)
fprintln(output, " Total Memory:", units.BytesSize(float64(info.MemTotal)))
fprintlnNonEmpty(output, " Name:", info.Name)
fprintlnNonEmpty(output, " ID:", info.ID)
fprintln(output, " Docker Root Dir:", info.DockerRootDir)
fprintln(output, " Debug Mode:", info.Debug)
// The daemon collects this information regardless if "debug" is
// enabled. Print the debugging information if either the daemon,
// or the client has debug enabled. We should probably improve this
// logic and print any of these if set (but some special rules are
// needed for file-descriptors, which may use "-1".
if info.Debug || debug.IsEnabled() {
fprintln(output, " File Descriptors:", info.NFd)
fprintln(output, " Goroutines:", info.NGoroutines)
fprintln(output, " System Time:", info.SystemTime)
fprintln(output, " EventsListeners:", info.NEventsListener)
}
fprintlnNonEmpty(output, " HTTP Proxy:", info.HTTPProxy)
fprintlnNonEmpty(output, " HTTPS Proxy:", info.HTTPSProxy)
fprintlnNonEmpty(output, " No Proxy:", info.NoProxy)
fprintlnNonEmpty(output, " Username:", info.UserName)
if len(info.Labels) > 0 {
fprintln(output, " Labels:")
for _, lbl := range info.Labels {
fprintln(output, " "+lbl)
}
}
fprintln(output, " Experimental:", info.ExperimentalBuild)
if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) {
fprintln(output, " Insecure Registries:")
for _, registryConfig := range info.RegistryConfig.IndexConfigs {
if !registryConfig.Secure {
fprintln(output, " "+registryConfig.Name)
}
}
for _, registryConfig := range info.RegistryConfig.InsecureRegistryCIDRs {
mask, _ := registryConfig.Mask.Size()
fprintf(output, " %s/%d\n", registryConfig.IP.String(), mask)
}
}
if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 {
fprintln(output, " Registry Mirrors:")
for _, mirror := range info.RegistryConfig.Mirrors {
fprintln(output, " "+mirror)
}
}
fprintln(output, " Live Restore Enabled:", info.LiveRestoreEnabled)
if info.ProductLicense != "" {
fprintln(output, " Product License:", info.ProductLicense)
}
if info.DefaultAddressPools != nil && len(info.DefaultAddressPools) > 0 {
fprintln(output, " Default Address Pools:")
for _, pool := range info.DefaultAddressPools {
fprintf(output, " Base: %s, Size: %d\n", pool.Base, pool.Size)
}
}
fprintln(output)
printServerWarnings(streams.Err(), info)
return errs
}
//nolint:gocyclo
func printSwarmInfo(output io.Writer, info system.Info) {
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive || info.Swarm.LocalNodeState == swarm.LocalNodeStateLocked {
return
}
fprintln(output, " NodeID:", info.Swarm.NodeID)
if info.Swarm.Error != "" {
fprintln(output, " Error:", info.Swarm.Error)
}
fprintln(output, " Is Manager:", info.Swarm.ControlAvailable)
if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError {
fprintln(output, " ClusterID:", info.Swarm.Cluster.ID)
fprintln(output, " Managers:", info.Swarm.Managers)
fprintln(output, " Nodes:", info.Swarm.Nodes)
var strAddrPool strings.Builder
if info.Swarm.Cluster.DefaultAddrPool != nil {
for _, p := range info.Swarm.Cluster.DefaultAddrPool {
strAddrPool.WriteString(p + " ")
}
fprintln(output, " Default Address Pool:", strAddrPool.String())
fprintln(output, " SubnetSize:", info.Swarm.Cluster.SubnetSize)
}
if info.Swarm.Cluster.DataPathPort > 0 {
fprintln(output, " Data Path Port:", info.Swarm.Cluster.DataPathPort)
}
fprintln(output, " Orchestration:")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
}
fprintln(output, " Task History Retention Limit:", taskHistoryRetentionLimit)
fprintln(output, " Raft:")
fprintln(output, " Snapshot Interval:", info.Swarm.Cluster.Spec.Raft.SnapshotInterval)
if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil {
fprintf(output, " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots)
}
fprintln(output, " Heartbeat Tick:", info.Swarm.Cluster.Spec.Raft.HeartbeatTick)
fprintln(output, " Election Tick:", info.Swarm.Cluster.Spec.Raft.ElectionTick)
fprintln(output, " Dispatcher:")
fprintln(output, " Heartbeat Period:", units.HumanDuration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))
fprintln(output, " CA Configuration:")
fprintln(output, " Expiry Duration:", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
fprintln(output, " Force Rotate:", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
if caCert := strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert); caCert != "" {
fprintf(output, " Signing CA Certificate: \n%s\n\n", caCert)
}
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
fprintln(output, " External CAs:")
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
fprintf(output, " %s: %s\n", entry.Protocol, entry.URL)
}
}
fprintln(output, " Autolock Managers:", info.Swarm.Cluster.Spec.EncryptionConfig.AutoLockManagers)
fprintln(output, " Root Rotation In Progress:", info.Swarm.Cluster.RootRotationInProgress)
}
fprintln(output, " Node Address:", info.Swarm.NodeAddr)
if len(info.Swarm.RemoteManagers) > 0 {
managers := []string{}
for _, entry := range info.Swarm.RemoteManagers {
managers = append(managers, entry.Addr)
}
sort.Strings(managers)
fprintln(output, " Manager Addresses:")
for _, entry := range managers {
fprintf(output, " %s\n", entry)
}
}
}
func printServerWarnings(stdErr io.Writer, info *dockerInfo) {
if versions.LessThan(info.ClientInfo.APIVersion, "1.42") {
printSecurityOptionsWarnings(stdErr, *info.Info)
}
if len(info.Warnings) > 0 {
fprintln(stdErr, strings.Join(info.Warnings, "\n"))
}
}
// printSecurityOptionsWarnings prints warnings based on the security options
// returned by the daemon.
//
// Deprecated: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printSecurityOptionsWarnings(stdErr io.Writer, info system.Info) {
if info.OSType == "windows" {
return
}
kvs, _ := system.DecodeSecurityOptions(info.SecurityOptions)
for _, so := range kvs {
if so.Name != "seccomp" {
continue
}
for _, o := range so.Options {
if o.Key == "profile" && o.Value != "default" && o.Value != "builtin" {
_, _ = fmt.Fprintln(stdErr, "WARNING: You're not using the default seccomp profile")
}
}
}
}
func formatInfo(output io.Writer, info dockerInfo, format string) error {
if format == formatter.JSONFormatKey {
format = formatter.JSONFormat
}
// Ensure slice/array fields render as `[]` not `null`
if info.ClientInfo != nil && info.ClientInfo.Plugins == nil {
info.ClientInfo.Plugins = make([]pluginmanager.Plugin, 0)
}
tmpl, err := templates.Parse(format)
if err != nil {
return cli.StatusError{
StatusCode: 64,
Status: "template parsing error: " + err.Error(),
}
}
err = tmpl.Execute(output, info)
fprintln(output)
return err
}
func fprintf(w io.Writer, format string, a ...any) {
_, _ = fmt.Fprintf(w, format, a...)
}
func fprintln(w io.Writer, a ...any) {
_, _ = fmt.Fprintln(w, a...)
}
func fprintlnNonEmpty(w io.Writer, label, value string) {
if value != "" {
_, _ = fmt.Fprintln(w, label, value)
}
}