Adding some utilities to print the output, to keep the linters happier without having to either suppress errors, or ignore them. Perhaps we should consider adding utilities for this on the "command.Streams" outputs. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
540 lines
15 KiB
Go
540 lines
15 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.Out(), 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.Out(), tc.dockerInfo, "json"))
|
|
golden.Assert(t, cli.OutBuffer().String(), tc.jsonGolden+".json.golden")
|
|
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkPrettyPrintInfo(b *testing.B) {
|
|
infoWithSwarm := sampleInfoNoSwarm
|
|
infoWithSwarm.Swarm = sampleSwarmInfo
|
|
|
|
dockerInfo := info{
|
|
Info: &infoWithSwarm,
|
|
ClientInfo: &clientInfo{
|
|
clientVersion: clientVersion{
|
|
Platform: &platformInfo{Name: "Docker Engine - Community"},
|
|
Version: "24.0.0",
|
|
Context: "default",
|
|
},
|
|
Debug: true,
|
|
},
|
|
}
|
|
cli := test.NewFakeCli(&fakeClient{})
|
|
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = prettyPrintInfo(cli, dockerInfo)
|
|
cli.ResetOutputBuffers()
|
|
}
|
|
}
|
|
|
|
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.Out(), 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)
|
|
})
|
|
}
|
|
}
|