Files
docker-cli/cli/command/system/info_test.go
Sebastiaan van Stijn 155f7d9e2b cli/command/system: add utilities for printing
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>
2023-05-02 16:55:12 +02:00

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: &registrytypes.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)
})
}
}