This patch adds additional information to the Client section of the output.
We were already outputting versions of CLI Plugins, and the Server, but not
for the Client.
Adding this information can help with bug-reports where the reporter only
provided the `docker info` output, or (e.g.) only `docker --version`. The
platform name helps identify what kind of builds the user has installed
(e.g. docker's docker-ce packages have "Docker Engine - Community" set
for this), although we should consider including "packager" information
as a more formalized field for this information.
Before this patch:
$ docker info
Client:
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.10.4
Path: /usr/libexec/docker/cli-plugins/docker-buildx
...
With this patch applied:
$ docker info
Client: Docker Engine - Community
Version: 24.0.0-dev
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.10.4
Path: /usr/libexec/docker/cli-plugins/docker-buildx
...
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
516 lines
14 KiB
Go
516 lines
14 KiB
Go
package system
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
|
"github.com/docker/cli/internal/test"
|
|
"github.com/docker/docker/api/types"
|
|
registrytypes "github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/golden"
|
|
)
|
|
|
|
// helper function that base64 decodes a string and ignores the error
|
|
func base64Decode(val string) []byte {
|
|
decoded, _ := base64.StdEncoding.DecodeString(val)
|
|
return decoded
|
|
}
|
|
|
|
const sampleID = "EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX"
|
|
|
|
var sampleInfoNoSwarm = types.Info{
|
|
ID: sampleID,
|
|
Containers: 0,
|
|
ContainersRunning: 0,
|
|
ContainersPaused: 0,
|
|
ContainersStopped: 0,
|
|
Images: 0,
|
|
Driver: "aufs",
|
|
DriverStatus: [][2]string{
|
|
{"Root Dir", "/var/lib/docker/aufs"},
|
|
{"Backing Filesystem", "extfs"},
|
|
{"Dirs", "0"},
|
|
{"Dirperm1 Supported", "true"},
|
|
},
|
|
SystemStatus: nil,
|
|
Plugins: types.PluginsInfo{
|
|
Volume: []string{"local"},
|
|
Network: []string{"bridge", "host", "macvlan", "null", "overlay"},
|
|
Authorization: nil,
|
|
Log: []string{"awslogs", "fluentd", "gcplogs", "gelf", "journald", "json-file", "logentries", "splunk", "syslog"},
|
|
},
|
|
MemoryLimit: true,
|
|
SwapLimit: true,
|
|
KernelMemory: true,
|
|
CPUCfsPeriod: true,
|
|
CPUCfsQuota: true,
|
|
CPUShares: true,
|
|
CPUSet: true,
|
|
IPv4Forwarding: true,
|
|
BridgeNfIptables: true,
|
|
BridgeNfIP6tables: true,
|
|
Debug: true,
|
|
NFd: 33,
|
|
OomKillDisable: true,
|
|
NGoroutines: 135,
|
|
SystemTime: "2017-08-24T17:44:34.077811894Z",
|
|
LoggingDriver: "json-file",
|
|
CgroupDriver: "cgroupfs",
|
|
NEventsListener: 0,
|
|
KernelVersion: "4.4.0-87-generic",
|
|
OperatingSystem: "Ubuntu 16.04.3 LTS",
|
|
OSVersion: "",
|
|
OSType: "linux",
|
|
Architecture: "x86_64",
|
|
IndexServerAddress: "https://index.docker.io/v1/",
|
|
RegistryConfig: ®istrytypes.ServiceConfig{
|
|
AllowNondistributableArtifactsCIDRs: nil,
|
|
AllowNondistributableArtifactsHostnames: nil,
|
|
InsecureRegistryCIDRs: []*registrytypes.NetIPNet{
|
|
{
|
|
IP: net.ParseIP("127.0.0.0"),
|
|
Mask: net.IPv4Mask(255, 0, 0, 0),
|
|
},
|
|
},
|
|
IndexConfigs: map[string]*registrytypes.IndexInfo{
|
|
"docker.io": {
|
|
Name: "docker.io",
|
|
Mirrors: nil,
|
|
Secure: true,
|
|
Official: true,
|
|
},
|
|
},
|
|
Mirrors: nil,
|
|
},
|
|
NCPU: 2,
|
|
MemTotal: 2097356800,
|
|
DockerRootDir: "/var/lib/docker",
|
|
HTTPProxy: "",
|
|
HTTPSProxy: "",
|
|
NoProxy: "",
|
|
Name: "system-sample",
|
|
Labels: []string{"provider=digitalocean"},
|
|
ExperimentalBuild: false,
|
|
ServerVersion: "17.06.1-ce",
|
|
Runtimes: map[string]types.Runtime{
|
|
"runc": {
|
|
Path: "docker-runc",
|
|
Args: nil,
|
|
},
|
|
},
|
|
DefaultRuntime: "runc",
|
|
Swarm: swarm.Info{LocalNodeState: "inactive"},
|
|
LiveRestoreEnabled: false,
|
|
Isolation: "",
|
|
InitBinary: "docker-init",
|
|
ContainerdCommit: types.Commit{
|
|
ID: "6e23458c129b551d5c9871e5174f6b1b7f6d1170",
|
|
Expected: "6e23458c129b551d5c9871e5174f6b1b7f6d1170",
|
|
},
|
|
RuncCommit: types.Commit{
|
|
ID: "810190ceaa507aa2727d7ae6f4790c76ec150bd2",
|
|
Expected: "810190ceaa507aa2727d7ae6f4790c76ec150bd2",
|
|
},
|
|
InitCommit: types.Commit{
|
|
ID: "949e6fa",
|
|
Expected: "949e6fa",
|
|
},
|
|
SecurityOptions: []string{"name=apparmor", "name=seccomp,profile=default"},
|
|
DefaultAddressPools: []types.NetworkAddressPool{
|
|
{
|
|
Base: "10.123.0.0/16",
|
|
Size: 24,
|
|
},
|
|
},
|
|
}
|
|
|
|
var sampleSwarmInfo = swarm.Info{
|
|
NodeID: "qo2dfdig9mmxqkawulggepdih",
|
|
NodeAddr: "165.227.107.89",
|
|
LocalNodeState: "active",
|
|
ControlAvailable: true,
|
|
Error: "",
|
|
RemoteManagers: []swarm.Peer{
|
|
{
|
|
NodeID: "qo2dfdig9mmxqkawulggepdih",
|
|
Addr: "165.227.107.89:2377",
|
|
},
|
|
},
|
|
Nodes: 1,
|
|
Managers: 1,
|
|
Cluster: &swarm.ClusterInfo{
|
|
ID: "9vs5ygs0gguyyec4iqf2314c0",
|
|
Meta: swarm.Meta{
|
|
Version: swarm.Version{Index: 11},
|
|
CreatedAt: time.Date(2017, 8, 24, 17, 34, 19, 278062352, time.UTC),
|
|
UpdatedAt: time.Date(2017, 8, 24, 17, 34, 42, 398815481, time.UTC),
|
|
},
|
|
Spec: swarm.Spec{
|
|
Annotations: swarm.Annotations{
|
|
Name: "default",
|
|
Labels: nil,
|
|
},
|
|
Orchestration: swarm.OrchestrationConfig{
|
|
TaskHistoryRetentionLimit: &[]int64{5}[0],
|
|
},
|
|
Raft: swarm.RaftConfig{
|
|
SnapshotInterval: 10000,
|
|
KeepOldSnapshots: &[]uint64{0}[0],
|
|
LogEntriesForSlowFollowers: 500,
|
|
ElectionTick: 3,
|
|
HeartbeatTick: 1,
|
|
},
|
|
Dispatcher: swarm.DispatcherConfig{
|
|
HeartbeatPeriod: 5000000000,
|
|
},
|
|
CAConfig: swarm.CAConfig{
|
|
NodeCertExpiry: 7776000000000000,
|
|
},
|
|
TaskDefaults: swarm.TaskDefaults{},
|
|
EncryptionConfig: swarm.EncryptionConfig{
|
|
AutoLockManagers: true,
|
|
},
|
|
},
|
|
TLSInfo: swarm.TLSInfo{
|
|
TrustRoot: `
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw
|
|
EzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy
|
|
OTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
|
A0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW
|
|
UfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
|
|
Af8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO
|
|
PQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH
|
|
1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8
|
|
-----END CERTIFICATE-----
|
|
`,
|
|
CertIssuerSubject: base64Decode("MBMxETAPBgNVBAMTCHN3YXJtLWNh"),
|
|
CertIssuerPublicKey: base64Decode(
|
|
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="),
|
|
},
|
|
RootRotationInProgress: false,
|
|
},
|
|
}
|
|
|
|
var samplePluginsInfo = []pluginmanager.Plugin{
|
|
{
|
|
Name: "goodplugin",
|
|
Path: "/path/to/docker-goodplugin",
|
|
Metadata: pluginmanager.Metadata{
|
|
SchemaVersion: "0.1.0",
|
|
ShortDescription: "unit test is good",
|
|
Vendor: "ACME Corp",
|
|
Version: "0.1.0",
|
|
},
|
|
},
|
|
{
|
|
Name: "unversionedplugin",
|
|
Path: "/path/to/docker-unversionedplugin",
|
|
Metadata: pluginmanager.Metadata{
|
|
SchemaVersion: "0.1.0",
|
|
ShortDescription: "this plugin has no version",
|
|
Vendor: "ACME Corp",
|
|
},
|
|
},
|
|
{
|
|
Name: "badplugin",
|
|
Path: "/path/to/docker-badplugin",
|
|
Err: pluginmanager.NewPluginError("something wrong"),
|
|
},
|
|
}
|
|
|
|
func TestPrettyPrintInfo(t *testing.T) {
|
|
infoWithSwarm := sampleInfoNoSwarm
|
|
infoWithSwarm.Swarm = sampleSwarmInfo
|
|
|
|
infoWithWarningsLinux := sampleInfoNoSwarm
|
|
infoWithWarningsLinux.MemoryLimit = false
|
|
infoWithWarningsLinux.SwapLimit = false
|
|
infoWithWarningsLinux.KernelMemory = false
|
|
infoWithWarningsLinux.OomKillDisable = false
|
|
infoWithWarningsLinux.CPUCfsQuota = false
|
|
infoWithWarningsLinux.CPUCfsPeriod = false
|
|
infoWithWarningsLinux.CPUShares = false
|
|
infoWithWarningsLinux.CPUSet = false
|
|
infoWithWarningsLinux.IPv4Forwarding = false
|
|
infoWithWarningsLinux.BridgeNfIptables = false
|
|
infoWithWarningsLinux.BridgeNfIP6tables = false
|
|
|
|
sampleInfoDaemonWarnings := sampleInfoNoSwarm
|
|
sampleInfoDaemonWarnings.Warnings = []string{
|
|
"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",
|
|
}
|
|
|
|
sampleInfoBadSecurity := sampleInfoNoSwarm
|
|
sampleInfoBadSecurity.SecurityOptions = []string{"foo="}
|
|
|
|
sampleInfoLabelsNil := sampleInfoNoSwarm
|
|
sampleInfoLabelsNil.Labels = nil
|
|
sampleInfoLabelsEmpty := sampleInfoNoSwarm
|
|
sampleInfoLabelsEmpty.Labels = []string{}
|
|
|
|
for _, tc := range []struct {
|
|
doc string
|
|
dockerInfo info
|
|
|
|
prettyGolden string
|
|
warningsGolden string
|
|
jsonGolden string
|
|
expectedError string
|
|
}{
|
|
{
|
|
doc: "info without swarm",
|
|
dockerInfo: info{
|
|
Info: &sampleInfoNoSwarm,
|
|
ClientInfo: &clientInfo{
|
|
clientVersion: clientVersion{
|
|
Platform: &platformInfo{Name: "Docker Engine - Community"},
|
|
Version: "24.0.0",
|
|
Context: "default",
|
|
},
|
|
Debug: true,
|
|
},
|
|
},
|
|
prettyGolden: "docker-info-no-swarm",
|
|
jsonGolden: "docker-info-no-swarm",
|
|
},
|
|
{
|
|
doc: "info with plugins",
|
|
dockerInfo: info{
|
|
Info: &sampleInfoNoSwarm,
|
|
ClientInfo: &clientInfo{
|
|
clientVersion: clientVersion{Context: "default"},
|
|
Plugins: samplePluginsInfo,
|
|
},
|
|
},
|
|
prettyGolden: "docker-info-plugins",
|
|
jsonGolden: "docker-info-plugins",
|
|
warningsGolden: "docker-info-plugins-warnings",
|
|
},
|
|
{
|
|
doc: "info with nil labels",
|
|
dockerInfo: info{
|
|
Info: &sampleInfoLabelsNil,
|
|
ClientInfo: &clientInfo{clientVersion: clientVersion{Context: "default"}},
|
|
},
|
|
prettyGolden: "docker-info-with-labels-nil",
|
|
},
|
|
{
|
|
doc: "info with empty labels",
|
|
dockerInfo: info{
|
|
Info: &sampleInfoLabelsEmpty,
|
|
ClientInfo: &clientInfo{clientVersion: clientVersion{Context: "default"}},
|
|
},
|
|
prettyGolden: "docker-info-with-labels-empty",
|
|
},
|
|
{
|
|
doc: "info with swarm",
|
|
dockerInfo: info{
|
|
Info: &infoWithSwarm,
|
|
ClientInfo: &clientInfo{
|
|
clientVersion: clientVersion{Context: "default"},
|
|
Debug: false,
|
|
},
|
|
},
|
|
prettyGolden: "docker-info-with-swarm",
|
|
jsonGolden: "docker-info-with-swarm",
|
|
},
|
|
{
|
|
doc: "info with legacy warnings",
|
|
dockerInfo: info{
|
|
Info: &infoWithWarningsLinux,
|
|
ClientInfo: &clientInfo{
|
|
clientVersion: clientVersion{
|
|
Platform: &platformInfo{Name: "Docker Engine - Community"},
|
|
Version: "24.0.0",
|
|
Context: "default",
|
|
},
|
|
Debug: true,
|
|
},
|
|
},
|
|
prettyGolden: "docker-info-no-swarm",
|
|
warningsGolden: "docker-info-warnings",
|
|
jsonGolden: "docker-info-legacy-warnings",
|
|
},
|
|
{
|
|
doc: "info with daemon warnings",
|
|
dockerInfo: info{
|
|
Info: &sampleInfoDaemonWarnings,
|
|
ClientInfo: &clientInfo{
|
|
clientVersion: clientVersion{
|
|
Platform: &platformInfo{Name: "Docker Engine - Community"},
|
|
Version: "24.0.0",
|
|
Context: "default",
|
|
},
|
|
Debug: true,
|
|
},
|
|
},
|
|
prettyGolden: "docker-info-no-swarm",
|
|
warningsGolden: "docker-info-warnings",
|
|
jsonGolden: "docker-info-daemon-warnings",
|
|
},
|
|
{
|
|
doc: "errors for both",
|
|
dockerInfo: info{
|
|
ServerErrors: []string{"a server error occurred"},
|
|
ClientErrors: []string{"a client error occurred"},
|
|
},
|
|
prettyGolden: "docker-info-errors",
|
|
jsonGolden: "docker-info-errors",
|
|
warningsGolden: "docker-info-errors-stderr",
|
|
expectedError: "errors pretty printing info",
|
|
},
|
|
{
|
|
doc: "bad security info",
|
|
dockerInfo: info{
|
|
Info: &sampleInfoBadSecurity,
|
|
ServerErrors: []string{"a server error occurred"},
|
|
ClientInfo: &clientInfo{Debug: false},
|
|
},
|
|
prettyGolden: "docker-info-badsec",
|
|
jsonGolden: "docker-info-badsec",
|
|
warningsGolden: "docker-info-badsec-stderr",
|
|
expectedError: "errors pretty printing info",
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.doc, func(t *testing.T) {
|
|
cli := test.NewFakeCli(&fakeClient{})
|
|
err := prettyPrintInfo(cli, tc.dockerInfo)
|
|
if tc.expectedError == "" {
|
|
assert.NilError(t, err)
|
|
} else {
|
|
assert.Error(t, err, tc.expectedError)
|
|
}
|
|
golden.Assert(t, cli.OutBuffer().String(), tc.prettyGolden+".golden")
|
|
if tc.warningsGolden != "" {
|
|
golden.Assert(t, cli.ErrBuffer().String(), tc.warningsGolden+".golden")
|
|
} else {
|
|
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
|
}
|
|
|
|
if tc.jsonGolden != "" {
|
|
cli = test.NewFakeCli(&fakeClient{})
|
|
assert.NilError(t, formatInfo(cli, tc.dockerInfo, "{{json .}}"))
|
|
golden.Assert(t, cli.OutBuffer().String(), tc.jsonGolden+".json.golden")
|
|
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
|
|
|
cli = test.NewFakeCli(&fakeClient{})
|
|
assert.NilError(t, formatInfo(cli, tc.dockerInfo, "json"))
|
|
golden.Assert(t, cli.OutBuffer().String(), tc.jsonGolden+".json.golden")
|
|
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatInfo(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
doc string
|
|
template string
|
|
expectedError string
|
|
expectedOut string
|
|
}{
|
|
{
|
|
doc: "basic",
|
|
template: "{{.ID}}",
|
|
expectedOut: sampleID + "\n",
|
|
},
|
|
{
|
|
doc: "syntax",
|
|
template: "{{}",
|
|
expectedError: `Status: template parsing error: template: :1: unexpected "}" in command, Code: 64`,
|
|
},
|
|
{
|
|
doc: "syntax",
|
|
template: "{{.badString}}",
|
|
expectedError: `template: :1:2: executing "" at <.badString>: can't evaluate field badString in type system.info`,
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.doc, func(t *testing.T) {
|
|
cli := test.NewFakeCli(&fakeClient{})
|
|
info := info{
|
|
Info: &sampleInfoNoSwarm,
|
|
ClientInfo: &clientInfo{Debug: true},
|
|
}
|
|
err := formatInfo(cli, info, tc.template)
|
|
if tc.expectedOut != "" {
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, cli.OutBuffer().String(), tc.expectedOut)
|
|
} else if tc.expectedError != "" {
|
|
assert.Error(t, err, tc.expectedError)
|
|
} else {
|
|
t.Fatal("test expected to neither pass nor fail")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|