/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package nodestatus import ( "errors" "fmt" "net" "sort" "strconv" "testing" "time" cadvisorapiv1 "github.com/google/cadvisor/info/v1" "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/uuid" fakecloud "k8s.io/kubernetes/pkg/cloudprovider/providers/fake" "k8s.io/kubernetes/pkg/kubelet/cm" kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" kubecontainertest "k8s.io/kubernetes/pkg/kubelet/container/testing" "k8s.io/kubernetes/pkg/kubelet/events" "k8s.io/kubernetes/pkg/kubelet/util/sliceutils" "k8s.io/kubernetes/pkg/version" "k8s.io/kubernetes/pkg/volume" volumetest "k8s.io/kubernetes/pkg/volume/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( testKubeletHostname = "127.0.0.1" ) // TODO(mtaufen): below is ported from the old kubelet_node_status_test.go code, potentially add more test coverage for NodeAddress setter in future func TestNodeAddress(t *testing.T) { cases := []struct { name string hostnameOverride bool nodeIP net.IP nodeAddresses []v1.NodeAddress expectedAddresses []v1.NodeAddress shouldError bool }{ { name: "A single InternalIP", nodeIP: net.ParseIP("10.1.1.1"), nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, shouldError: false, }, { name: "NodeIP is external", nodeIP: net.ParseIP("55.55.55.55"), nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, shouldError: false, }, { // Accommodating #45201 and #49202 name: "InternalIP and ExternalIP are the same", nodeIP: net.ParseIP("55.55.55.55"), nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "44.44.44.44"}, {Type: v1.NodeExternalIP, Address: "44.44.44.44"}, {Type: v1.NodeInternalIP, Address: "55.55.55.55"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "55.55.55.55"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, shouldError: false, }, { name: "An Internal/ExternalIP, an Internal/ExternalDNS", nodeIP: net.ParseIP("10.1.1.1"), nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeInternalDNS, Address: "ip-10-1-1-1.us-west-2.compute.internal"}, {Type: v1.NodeExternalDNS, Address: "ec2-55-55-55-55.us-west-2.compute.amazonaws.com"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeInternalDNS, Address: "ip-10-1-1-1.us-west-2.compute.internal"}, {Type: v1.NodeExternalDNS, Address: "ec2-55-55-55-55.us-west-2.compute.amazonaws.com"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, shouldError: false, }, { name: "An Internal with multiple internal IPs", nodeIP: net.ParseIP("10.1.1.1"), nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeInternalIP, Address: "10.2.2.2"}, {Type: v1.NodeInternalIP, Address: "10.3.3.3"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, shouldError: false, }, { name: "An InternalIP that isn't valid: should error", nodeIP: net.ParseIP("10.2.2.2"), nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, }, expectedAddresses: nil, shouldError: true, }, { name: "no cloud reported hostnames", nodeAddresses: []v1.NodeAddress{}, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeHostName, Address: testKubeletHostname}, // detected hostname is auto-added in the absence of cloud-reported hostnames }, shouldError: false, }, { name: "cloud reports hostname, no override", nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: "cloud-host"}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: "cloud-host"}, // cloud-reported hostname wins over detected hostname }, shouldError: false, }, { name: "cloud reports hostname, overridden", nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeHostName, Address: "cloud-host"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, // hostname-override wins over cloud-reported hostname {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, }, hostnameOverride: true, shouldError: false, }, { name: "cloud doesn't report hostname, no override, detected hostname mismatch", nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, // detected hostname is not auto-added if it doesn't match any cloud-reported addresses }, shouldError: false, }, { name: "cloud doesn't report hostname, no override, detected hostname match", nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeExternalDNS, Address: testKubeletHostname}, // cloud-reported address value matches detected hostname }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeExternalDNS, Address: testKubeletHostname}, {Type: v1.NodeHostName, Address: testKubeletHostname}, // detected hostname gets auto-added }, shouldError: false, }, { name: "cloud doesn't report hostname, hostname override, hostname mismatch", nodeAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, }, expectedAddresses: []v1.NodeAddress{ {Type: v1.NodeInternalIP, Address: "10.1.1.1"}, {Type: v1.NodeExternalIP, Address: "55.55.55.55"}, {Type: v1.NodeHostName, Address: testKubeletHostname}, // overridden hostname gets auto-added }, hostnameOverride: true, shouldError: false, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { // testCase setup existingNode := &v1.Node{ ObjectMeta: metav1.ObjectMeta{Name: testKubeletHostname, Annotations: make(map[string]string)}, Spec: v1.NodeSpec{}, } nodeIP := testCase.nodeIP nodeIPValidator := func(nodeIP net.IP) error { return nil } hostname := testKubeletHostname externalCloudProvider := false cloud := &fakecloud.FakeCloud{ Addresses: testCase.nodeAddresses, Err: nil, } nodeAddressesFunc := func() ([]v1.NodeAddress, error) { return testCase.nodeAddresses, nil } // construct setter setter := NodeAddress(nodeIP, nodeIPValidator, hostname, testCase.hostnameOverride, externalCloudProvider, cloud, nodeAddressesFunc) // call setter on existing node err := setter(existingNode) if err != nil && !testCase.shouldError { t.Fatalf("unexpected error: %v", err) } else if err != nil && testCase.shouldError { // expected an error, and got one, so just return early here return } // Sort both sets for consistent equality sortNodeAddresses(testCase.expectedAddresses) sortNodeAddresses(existingNode.Status.Addresses) assert.True(t, apiequality.Semantic.DeepEqual(testCase.expectedAddresses, existingNode.Status.Addresses), "Diff: %s", diff.ObjectDiff(testCase.expectedAddresses, existingNode.Status.Addresses)) }) } } func TestMachineInfo(t *testing.T) { const nodeName = "test-node" type dprc struct { capacity v1.ResourceList allocatable v1.ResourceList inactive []string } cases := []struct { desc string node *v1.Node maxPods int podsPerCore int machineInfo *cadvisorapiv1.MachineInfo machineInfoError error capacity v1.ResourceList devicePluginResourceCapacity dprc nodeAllocatableReservation v1.ResourceList expectNode *v1.Node expectEvents []testEvent }{ { desc: "machine identifiers, basic capacity and allocatable", node: &v1.Node{}, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ MachineID: "MachineID", SystemUUID: "SystemUUID", NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ NodeInfo: v1.NodeSystemInfo{ MachineID: "MachineID", SystemUUID: "SystemUUID", }, Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, }, }, }, { desc: "podsPerCore greater than zero, but less than maxPods/cores", node: &v1.Node{}, maxPods: 10, podsPerCore: 4, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(8, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(8, resource.DecimalSI), }, }, }, }, { desc: "podsPerCore greater than maxPods/cores", node: &v1.Node{}, maxPods: 10, podsPerCore: 6, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI), }, }, }, }, { desc: "allocatable should equal capacity minus reservations", node: &v1.Node{}, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, nodeAllocatableReservation: v1.ResourceList{ // reserve 1 unit for each resource v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(1, resource.DecimalSI), v1.ResourceEphemeralStorage: *resource.NewQuantity(1, resource.BinarySI), }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(1999, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1023, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(109, resource.DecimalSI), }, }, }, }, { desc: "allocatable memory does not double-count hugepages reservations", node: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ // it's impossible on any real system to reserve 1 byte, // but we just need to test that the setter does the math v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1, resource.BinarySI), }, }, }, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), // memory has 1-unit difference for hugepages reservation v1.ResourceMemory: *resource.NewQuantity(1023, resource.BinarySI), v1.ResourceHugePagesPrefix + "test": *resource.NewQuantity(1, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, }, }, }, { desc: "negative capacity resources should be set to 0 in allocatable", node: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ "negative-resource": *resource.NewQuantity(-1, resource.BinarySI), }, }, }, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), "negative-resource": *resource.NewQuantity(-1, resource.BinarySI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), "negative-resource": *resource.NewQuantity(0, resource.BinarySI), }, }, }, }, { desc: "ephemeral storage is reflected in capacity and allocatable", node: &v1.Node{}, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, capacity: v1.ResourceList{ v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), }, }, }, }, { desc: "device plugin resources are reflected in capacity and allocatable", node: &v1.Node{}, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, devicePluginResourceCapacity: dprc{ capacity: v1.ResourceList{ "device-plugin": *resource.NewQuantity(1, resource.BinarySI), }, allocatable: v1.ResourceList{ "device-plugin": *resource.NewQuantity(1, resource.BinarySI), }, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), "device-plugin": *resource.NewQuantity(1, resource.BinarySI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), "device-plugin": *resource.NewQuantity(1, resource.BinarySI), }, }, }, }, { desc: "inactive device plugin resources should have their capacity set to 0", node: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ "inactive": *resource.NewQuantity(1, resource.BinarySI), }, }, }, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, devicePluginResourceCapacity: dprc{ inactive: []string{"inactive"}, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), "inactive": *resource.NewQuantity(0, resource.BinarySI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), "inactive": *resource.NewQuantity(0, resource.BinarySI), }, }, }, }, { desc: "extended resources not present in capacity are removed from allocatable", node: &v1.Node{ Status: v1.NodeStatus{ Allocatable: v1.ResourceList{ "example.com/extended": *resource.NewQuantity(1, resource.BinarySI), }, }, }, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, }, }, }, { desc: "on failure to get machine info, allocatable and capacity for memory and cpu are set to 0, pods to maxPods", node: &v1.Node{}, maxPods: 110, // podsPerCore is not accounted for when getting machine info fails podsPerCore: 1, machineInfoError: fmt.Errorf("foo"), expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), v1.ResourceMemory: resource.MustParse("0Gi"), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI), v1.ResourceMemory: resource.MustParse("0Gi"), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, }, }, }, { desc: "node reboot event is recorded", node: &v1.Node{ Status: v1.NodeStatus{ NodeInfo: v1.NodeSystemInfo{ BootID: "foo", }, }, }, maxPods: 110, machineInfo: &cadvisorapiv1.MachineInfo{ BootID: "bar", NumCores: 2, MemoryCapacity: 1024, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ NodeInfo: v1.NodeSystemInfo{ BootID: "bar", }, Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, Allocatable: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), }, }, }, expectEvents: []testEvent{ { eventType: v1.EventTypeWarning, event: events.NodeRebooted, message: fmt.Sprintf("Node %s has been rebooted, boot id: %s", nodeName, "bar"), }, }, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { machineInfoFunc := func() (*cadvisorapiv1.MachineInfo, error) { return tc.machineInfo, tc.machineInfoError } capacityFunc := func() v1.ResourceList { return tc.capacity } devicePluginResourceCapacityFunc := func() (v1.ResourceList, v1.ResourceList, []string) { c := tc.devicePluginResourceCapacity return c.capacity, c.allocatable, c.inactive } nodeAllocatableReservationFunc := func() v1.ResourceList { return tc.nodeAllocatableReservation } events := []testEvent{} recordEventFunc := func(eventType, event, message string) { events = append(events, testEvent{ eventType: eventType, event: event, message: message, }) } // construct setter setter := MachineInfo(nodeName, tc.maxPods, tc.podsPerCore, machineInfoFunc, capacityFunc, devicePluginResourceCapacityFunc, nodeAllocatableReservationFunc, recordEventFunc) // call setter on node if err := setter(tc.node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected node assert.True(t, apiequality.Semantic.DeepEqual(tc.expectNode, tc.node), "Diff: %s", diff.ObjectDiff(tc.expectNode, tc.node)) // check expected events require.Equal(t, len(tc.expectEvents), len(events)) for i := range tc.expectEvents { assert.Equal(t, tc.expectEvents[i], events[i]) } }) } } func TestVersionInfo(t *testing.T) { cases := []struct { desc string node *v1.Node versionInfo *cadvisorapiv1.VersionInfo versionInfoError error runtimeType string runtimeVersion kubecontainer.Version runtimeVersionError error expectNode *v1.Node expectError error }{ { desc: "versions set in node info", node: &v1.Node{}, versionInfo: &cadvisorapiv1.VersionInfo{ KernelVersion: "KernelVersion", ContainerOsVersion: "ContainerOSVersion", }, runtimeType: "RuntimeType", runtimeVersion: &kubecontainertest.FakeVersion{ Version: "RuntimeVersion", }, expectNode: &v1.Node{ Status: v1.NodeStatus{ NodeInfo: v1.NodeSystemInfo{ KernelVersion: "KernelVersion", OSImage: "ContainerOSVersion", ContainerRuntimeVersion: "RuntimeType://RuntimeVersion", KubeletVersion: version.Get().String(), KubeProxyVersion: version.Get().String(), }, }, }, }, { desc: "error getting version info", node: &v1.Node{}, versionInfoError: fmt.Errorf("foo"), expectNode: &v1.Node{}, expectError: fmt.Errorf("error getting version info: foo"), }, { desc: "error getting runtime version results in Unknown runtime", node: &v1.Node{}, versionInfo: &cadvisorapiv1.VersionInfo{}, runtimeType: "RuntimeType", runtimeVersionError: fmt.Errorf("foo"), expectNode: &v1.Node{ Status: v1.NodeStatus{ NodeInfo: v1.NodeSystemInfo{ ContainerRuntimeVersion: "RuntimeType://Unknown", KubeletVersion: version.Get().String(), KubeProxyVersion: version.Get().String(), }, }, }, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { versionInfoFunc := func() (*cadvisorapiv1.VersionInfo, error) { return tc.versionInfo, tc.versionInfoError } runtimeTypeFunc := func() string { return tc.runtimeType } runtimeVersionFunc := func() (kubecontainer.Version, error) { return tc.runtimeVersion, tc.runtimeVersionError } // construct setter setter := VersionInfo(versionInfoFunc, runtimeTypeFunc, runtimeVersionFunc) // call setter on node err := setter(tc.node) require.Equal(t, tc.expectError, err) // check expected node assert.True(t, apiequality.Semantic.DeepEqual(tc.expectNode, tc.node), "Diff: %s", diff.ObjectDiff(tc.expectNode, tc.node)) }) } } func TestImages(t *testing.T) { const ( minImageSize = 23 * 1024 * 1024 maxImageSize = 1000 * 1024 * 1024 ) cases := []struct { desc string maxImages int32 imageList []kubecontainer.Image imageListError error expectError error }{ { desc: "max images enforced", maxImages: 1, imageList: makeImageList(2, 1, minImageSize, maxImageSize), }, { desc: "no max images cap for -1", maxImages: -1, imageList: makeImageList(2, 1, minImageSize, maxImageSize), }, { desc: "max names per image enforced", maxImages: -1, imageList: makeImageList(1, MaxNamesPerImageInNodeStatus+1, minImageSize, maxImageSize), }, { desc: "images are sorted by size, descending", maxImages: -1, // makeExpectedImageList will sort them for expectedNode when the test case is run imageList: []kubecontainer.Image{{Size: 3}, {Size: 1}, {Size: 4}, {Size: 2}}, }, { desc: "repo digests and tags both show up in image names", maxImages: -1, // makeExpectedImageList will use both digests and tags imageList: []kubecontainer.Image{ { RepoDigests: []string{"foo", "bar"}, RepoTags: []string{"baz", "quux"}, }, }, }, { desc: "error getting image list, image list on node is reset to empty", maxImages: -1, imageListError: fmt.Errorf("foo"), expectError: fmt.Errorf("error getting image list: foo"), }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { imageListFunc := func() ([]kubecontainer.Image, error) { // today, imageListFunc is expected to return a sorted list, // but we may choose to sort in the setter at some future point // (e.g. if the image cache stopped sorting for us) sort.Sort(sliceutils.ByImageSize(tc.imageList)) return tc.imageList, tc.imageListError } // construct setter setter := Images(tc.maxImages, imageListFunc) // call setter on node node := &v1.Node{} err := setter(node) require.Equal(t, tc.expectError, err) // check expected node, image list should be reset to empty when there is an error expectNode := &v1.Node{} if err == nil { expectNode.Status.Images = makeExpectedImageList(tc.imageList, tc.maxImages, MaxNamesPerImageInNodeStatus) } assert.True(t, apiequality.Semantic.DeepEqual(expectNode, node), "Diff: %s", diff.ObjectDiff(expectNode, node)) }) } } func TestReadyCondition(t *testing.T) { now := time.Now() before := now.Add(-time.Second) nowFunc := func() time.Time { return now } withCapacity := &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(10E9, resource.BinarySI), v1.ResourcePods: *resource.NewQuantity(100, resource.DecimalSI), v1.ResourceEphemeralStorage: *resource.NewQuantity(5000, resource.BinarySI), }, }, } cases := []struct { desc string node *v1.Node runtimeErrors error networkErrors error storageErrors error appArmorValidateHostFunc func() error cmStatus cm.Status expectConditions []v1.NodeCondition expectEvents []testEvent }{ { desc: "new, ready", node: withCapacity.DeepCopy(), expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", now, now)}, // TODO(mtaufen): The current behavior is that we don't send an event for the initial NodeReady condition, // the reason for this is unclear, so we may want to actually send an event, and change these test cases // to ensure an event is sent. }, { desc: "new, ready: apparmor validator passed", node: withCapacity.DeepCopy(), appArmorValidateHostFunc: func() error { return nil }, expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status. AppArmor enabled", now, now)}, }, { desc: "new, ready: apparmor validator failed", node: withCapacity.DeepCopy(), appArmorValidateHostFunc: func() error { return fmt.Errorf("foo") }, // absence of an additional message is understood to mean that AppArmor is disabled expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", now, now)}, }, { desc: "new, ready: soft requirement warning", node: withCapacity.DeepCopy(), cmStatus: cm.Status{ SoftRequirements: fmt.Errorf("foo"), }, expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status. WARNING: foo", now, now)}, }, { desc: "new, not ready: storage errors", node: withCapacity.DeepCopy(), storageErrors: errors.New("some storage error"), expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "some storage error", now, now)}, }, { desc: "new, not ready: runtime and network errors", node: withCapacity.DeepCopy(), runtimeErrors: errors.New("runtime"), networkErrors: errors.New("network"), expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "[runtime, network]", now, now)}, }, { desc: "new, not ready: missing capacities", node: &v1.Node{}, expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "Missing node capacity for resources: cpu, memory, pods, ephemeral-storage", now, now)}, }, // the transition tests ensure timestamps are set correctly, no need to test the entire condition matrix in this section { desc: "transition to ready", node: func() *v1.Node { node := withCapacity.DeepCopy() node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(false, "", before, before)} return node }(), expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: events.NodeReady, }, }, }, { desc: "transition to not ready", node: func() *v1.Node { node := withCapacity.DeepCopy() node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(true, "", before, before)} return node }(), runtimeErrors: errors.New("foo"), expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "foo", now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: events.NodeNotReady, }, }, }, { desc: "ready, no transition", node: func() *v1.Node { node := withCapacity.DeepCopy() node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(true, "", before, before)} return node }(), expectConditions: []v1.NodeCondition{*makeReadyCondition(true, "kubelet is posting ready status", before, now)}, expectEvents: []testEvent{}, }, { desc: "not ready, no transition", node: func() *v1.Node { node := withCapacity.DeepCopy() node.Status.Conditions = []v1.NodeCondition{*makeReadyCondition(false, "", before, before)} return node }(), runtimeErrors: errors.New("foo"), expectConditions: []v1.NodeCondition{*makeReadyCondition(false, "foo", before, now)}, expectEvents: []testEvent{}, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { runtimeErrorsFunc := func() error { return tc.runtimeErrors } networkErrorsFunc := func() error { return tc.networkErrors } storageErrorsFunc := func() error { return tc.storageErrors } cmStatusFunc := func() cm.Status { return tc.cmStatus } events := []testEvent{} recordEventFunc := func(eventType, event string) { events = append(events, testEvent{ eventType: eventType, event: event, }) } // construct setter setter := ReadyCondition(nowFunc, runtimeErrorsFunc, networkErrorsFunc, storageErrorsFunc, tc.appArmorValidateHostFunc, cmStatusFunc, recordEventFunc) // call setter on node if err := setter(tc.node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected condition assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), "Diff: %s", diff.ObjectDiff(tc.expectConditions, tc.node.Status.Conditions)) // check expected events require.Equal(t, len(tc.expectEvents), len(events)) for i := range tc.expectEvents { assert.Equal(t, tc.expectEvents[i], events[i]) } }) } } func TestMemoryPressureCondition(t *testing.T) { now := time.Now() before := now.Add(-time.Second) nowFunc := func() time.Time { return now } cases := []struct { desc string node *v1.Node pressure bool expectConditions []v1.NodeCondition expectEvents []testEvent }{ { desc: "new, no pressure", node: &v1.Node{}, pressure: false, expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasSufficientMemory", }, }, }, { desc: "new, pressure", node: &v1.Node{}, pressure: true, expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasInsufficientMemory", }, }, }, { desc: "transition to pressure", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, before, before)}, }, }, pressure: true, expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasInsufficientMemory", }, }, }, { desc: "transition to no pressure", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, before, before)}, }, }, pressure: false, expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasSufficientMemory", }, }, }, { desc: "pressure, no transition", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, before, before)}, }, }, pressure: true, expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(true, before, now)}, expectEvents: []testEvent{}, }, { desc: "no pressure, no transition", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, before, before)}, }, }, pressure: false, expectConditions: []v1.NodeCondition{*makeMemoryPressureCondition(false, before, now)}, expectEvents: []testEvent{}, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { events := []testEvent{} recordEventFunc := func(eventType, event string) { events = append(events, testEvent{ eventType: eventType, event: event, }) } pressureFunc := func() bool { return tc.pressure } // construct setter setter := MemoryPressureCondition(nowFunc, pressureFunc, recordEventFunc) // call setter on node if err := setter(tc.node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected condition assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), "Diff: %s", diff.ObjectDiff(tc.expectConditions, tc.node.Status.Conditions)) // check expected events require.Equal(t, len(tc.expectEvents), len(events)) for i := range tc.expectEvents { assert.Equal(t, tc.expectEvents[i], events[i]) } }) } } func TestPIDPressureCondition(t *testing.T) { now := time.Now() before := now.Add(-time.Second) nowFunc := func() time.Time { return now } cases := []struct { desc string node *v1.Node pressure bool expectConditions []v1.NodeCondition expectEvents []testEvent }{ { desc: "new, no pressure", node: &v1.Node{}, pressure: false, expectConditions: []v1.NodeCondition{*makePIDPressureCondition(false, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasSufficientPID", }, }, }, { desc: "new, pressure", node: &v1.Node{}, pressure: true, expectConditions: []v1.NodeCondition{*makePIDPressureCondition(true, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasInsufficientPID", }, }, }, { desc: "transition to pressure", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makePIDPressureCondition(false, before, before)}, }, }, pressure: true, expectConditions: []v1.NodeCondition{*makePIDPressureCondition(true, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasInsufficientPID", }, }, }, { desc: "transition to no pressure", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makePIDPressureCondition(true, before, before)}, }, }, pressure: false, expectConditions: []v1.NodeCondition{*makePIDPressureCondition(false, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasSufficientPID", }, }, }, { desc: "pressure, no transition", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makePIDPressureCondition(true, before, before)}, }, }, pressure: true, expectConditions: []v1.NodeCondition{*makePIDPressureCondition(true, before, now)}, expectEvents: []testEvent{}, }, { desc: "no pressure, no transition", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makePIDPressureCondition(false, before, before)}, }, }, pressure: false, expectConditions: []v1.NodeCondition{*makePIDPressureCondition(false, before, now)}, expectEvents: []testEvent{}, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { events := []testEvent{} recordEventFunc := func(eventType, event string) { events = append(events, testEvent{ eventType: eventType, event: event, }) } pressureFunc := func() bool { return tc.pressure } // construct setter setter := PIDPressureCondition(nowFunc, pressureFunc, recordEventFunc) // call setter on node if err := setter(tc.node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected condition assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), "Diff: %s", diff.ObjectDiff(tc.expectConditions, tc.node.Status.Conditions)) // check expected events require.Equal(t, len(tc.expectEvents), len(events)) for i := range tc.expectEvents { assert.Equal(t, tc.expectEvents[i], events[i]) } }) } } func TestDiskPressureCondition(t *testing.T) { now := time.Now() before := now.Add(-time.Second) nowFunc := func() time.Time { return now } cases := []struct { desc string node *v1.Node pressure bool expectConditions []v1.NodeCondition expectEvents []testEvent }{ { desc: "new, no pressure", node: &v1.Node{}, pressure: false, expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(false, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasNoDiskPressure", }, }, }, { desc: "new, pressure", node: &v1.Node{}, pressure: true, expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(true, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasDiskPressure", }, }, }, { desc: "transition to pressure", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeDiskPressureCondition(false, before, before)}, }, }, pressure: true, expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(true, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasDiskPressure", }, }, }, { desc: "transition to no pressure", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeDiskPressureCondition(true, before, before)}, }, }, pressure: false, expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(false, now, now)}, expectEvents: []testEvent{ { eventType: v1.EventTypeNormal, event: "NodeHasNoDiskPressure", }, }, }, { desc: "pressure, no transition", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeDiskPressureCondition(true, before, before)}, }, }, pressure: true, expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(true, before, now)}, expectEvents: []testEvent{}, }, { desc: "no pressure, no transition", node: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{*makeDiskPressureCondition(false, before, before)}, }, }, pressure: false, expectConditions: []v1.NodeCondition{*makeDiskPressureCondition(false, before, now)}, expectEvents: []testEvent{}, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { events := []testEvent{} recordEventFunc := func(eventType, event string) { events = append(events, testEvent{ eventType: eventType, event: event, }) } pressureFunc := func() bool { return tc.pressure } // construct setter setter := DiskPressureCondition(nowFunc, pressureFunc, recordEventFunc) // call setter on node if err := setter(tc.node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected condition assert.True(t, apiequality.Semantic.DeepEqual(tc.expectConditions, tc.node.Status.Conditions), "Diff: %s", diff.ObjectDiff(tc.expectConditions, tc.node.Status.Conditions)) // check expected events require.Equal(t, len(tc.expectEvents), len(events)) for i := range tc.expectEvents { assert.Equal(t, tc.expectEvents[i], events[i]) } }) } } func TestVolumesInUse(t *testing.T) { withVolumesInUse := &v1.Node{ Status: v1.NodeStatus{ VolumesInUse: []v1.UniqueVolumeName{"foo"}, }, } cases := []struct { desc string node *v1.Node synced bool volumesInUse []v1.UniqueVolumeName expectVolumesInUse []v1.UniqueVolumeName }{ { desc: "synced", node: withVolumesInUse.DeepCopy(), synced: true, volumesInUse: []v1.UniqueVolumeName{"bar"}, expectVolumesInUse: []v1.UniqueVolumeName{"bar"}, }, { desc: "not synced", node: withVolumesInUse.DeepCopy(), synced: false, volumesInUse: []v1.UniqueVolumeName{"bar"}, expectVolumesInUse: []v1.UniqueVolumeName{"foo"}, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { syncedFunc := func() bool { return tc.synced } volumesInUseFunc := func() []v1.UniqueVolumeName { return tc.volumesInUse } // construct setter setter := VolumesInUse(syncedFunc, volumesInUseFunc) // call setter on node if err := setter(tc.node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected volumes assert.True(t, apiequality.Semantic.DeepEqual(tc.expectVolumesInUse, tc.node.Status.VolumesInUse), "Diff: %s", diff.ObjectDiff(tc.expectVolumesInUse, tc.node.Status.VolumesInUse)) }) } } func TestVolumeLimits(t *testing.T) { const ( volumeLimitKey = "attachable-volumes-fake-provider" volumeLimitVal = 16 ) var cases = []struct { desc string volumePluginList []volume.VolumePluginWithAttachLimits expectNode *v1.Node }{ { desc: "translate limits to capacity and allocatable for plugins that return successfully from GetVolumeLimits", volumePluginList: []volume.VolumePluginWithAttachLimits{ &volumetest.FakeVolumePlugin{ VolumeLimits: map[string]int64{volumeLimitKey: volumeLimitVal}, }, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Capacity: v1.ResourceList{ volumeLimitKey: *resource.NewQuantity(volumeLimitVal, resource.DecimalSI), }, Allocatable: v1.ResourceList{ volumeLimitKey: *resource.NewQuantity(volumeLimitVal, resource.DecimalSI), }, }, }, }, { desc: "skip plugins that return errors from GetVolumeLimits", volumePluginList: []volume.VolumePluginWithAttachLimits{ &volumetest.FakeVolumePlugin{ VolumeLimitsError: fmt.Errorf("foo"), }, }, expectNode: &v1.Node{}, }, { desc: "no plugins", expectNode: &v1.Node{}, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { volumePluginListFunc := func() []volume.VolumePluginWithAttachLimits { return tc.volumePluginList } // construct setter setter := VolumeLimits(volumePluginListFunc) // call setter on node node := &v1.Node{} if err := setter(node); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected node assert.True(t, apiequality.Semantic.DeepEqual(tc.expectNode, node), "Diff: %s", diff.ObjectDiff(tc.expectNode, node)) }) } } func TestRemoveOutOfDiskCondition(t *testing.T) { now := time.Now() var cases = []struct { desc string inputNode *v1.Node expectNode *v1.Node }{ { desc: "should remove stale OutOfDiskCondition from node status", inputNode: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{ *makeMemoryPressureCondition(false, now, now), { Type: v1.NodeOutOfDisk, Status: v1.ConditionFalse, }, *makeDiskPressureCondition(false, now, now), }, }, }, expectNode: &v1.Node{ Status: v1.NodeStatus{ Conditions: []v1.NodeCondition{ *makeMemoryPressureCondition(false, now, now), *makeDiskPressureCondition(false, now, now), }, }, }, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { // construct setter setter := RemoveOutOfDiskCondition() // call setter on node if err := setter(tc.inputNode); err != nil { t.Fatalf("unexpected error: %v", err) } // check expected node assert.True(t, apiequality.Semantic.DeepEqual(tc.expectNode, tc.inputNode), "Diff: %s", diff.ObjectDiff(tc.expectNode, tc.inputNode)) }) } } // Test Helpers: // sortableNodeAddress is a type for sorting []v1.NodeAddress type sortableNodeAddress []v1.NodeAddress func (s sortableNodeAddress) Len() int { return len(s) } func (s sortableNodeAddress) Less(i, j int) bool { return (string(s[i].Type) + s[i].Address) < (string(s[j].Type) + s[j].Address) } func (s sortableNodeAddress) Swap(i, j int) { s[j], s[i] = s[i], s[j] } func sortNodeAddresses(addrs sortableNodeAddress) { sort.Sort(addrs) } // testEvent is used to record events for tests type testEvent struct { eventType string event string message string } // makeImageList randomly generates a list of images with the given count func makeImageList(numImages, numTags, minSize, maxSize int32) []kubecontainer.Image { images := make([]kubecontainer.Image, numImages) for i := range images { image := &images[i] image.ID = string(uuid.NewUUID()) image.RepoTags = makeImageTags(numTags) image.Size = rand.Int63nRange(int64(minSize), int64(maxSize+1)) } return images } func makeExpectedImageList(imageList []kubecontainer.Image, maxImages, maxNames int32) []v1.ContainerImage { // copy the imageList, we do not want to mutate it in-place and accidentally edit a test case images := make([]kubecontainer.Image, len(imageList)) copy(images, imageList) // sort images by size sort.Sort(sliceutils.ByImageSize(images)) // convert to []v1.ContainerImage and truncate the list of names expectedImages := make([]v1.ContainerImage, len(images)) for i := range images { image := &images[i] expectedImage := &expectedImages[i] names := append(image.RepoDigests, image.RepoTags...) if len(names) > int(maxNames) { names = names[0:maxNames] } expectedImage.Names = names expectedImage.SizeBytes = image.Size } // -1 means no limit, truncate result list if necessary. if maxImages > -1 && int(maxImages) < len(expectedImages) { return expectedImages[0:maxImages] } return expectedImages } func makeImageTags(num int32) []string { tags := make([]string, num) for i := range tags { tags[i] = "k8s.gcr.io:v" + strconv.Itoa(i) } return tags } func makeReadyCondition(ready bool, message string, transition, heartbeat time.Time) *v1.NodeCondition { if ready { return &v1.NodeCondition{ Type: v1.NodeReady, Status: v1.ConditionTrue, Reason: "KubeletReady", Message: message, LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } return &v1.NodeCondition{ Type: v1.NodeReady, Status: v1.ConditionFalse, Reason: "KubeletNotReady", Message: message, LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } func makeMemoryPressureCondition(pressure bool, transition, heartbeat time.Time) *v1.NodeCondition { if pressure { return &v1.NodeCondition{ Type: v1.NodeMemoryPressure, Status: v1.ConditionTrue, Reason: "KubeletHasInsufficientMemory", Message: "kubelet has insufficient memory available", LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } return &v1.NodeCondition{ Type: v1.NodeMemoryPressure, Status: v1.ConditionFalse, Reason: "KubeletHasSufficientMemory", Message: "kubelet has sufficient memory available", LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } func makePIDPressureCondition(pressure bool, transition, heartbeat time.Time) *v1.NodeCondition { if pressure { return &v1.NodeCondition{ Type: v1.NodePIDPressure, Status: v1.ConditionTrue, Reason: "KubeletHasInsufficientPID", Message: "kubelet has insufficient PID available", LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } return &v1.NodeCondition{ Type: v1.NodePIDPressure, Status: v1.ConditionFalse, Reason: "KubeletHasSufficientPID", Message: "kubelet has sufficient PID available", LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } func makeDiskPressureCondition(pressure bool, transition, heartbeat time.Time) *v1.NodeCondition { if pressure { return &v1.NodeCondition{ Type: v1.NodeDiskPressure, Status: v1.ConditionTrue, Reason: "KubeletHasDiskPressure", Message: "kubelet has disk pressure", LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } } return &v1.NodeCondition{ Type: v1.NodeDiskPressure, Status: v1.ConditionFalse, Reason: "KubeletHasNoDiskPressure", Message: "kubelet has no disk pressure", LastTransitionTime: metav1.NewTime(transition), LastHeartbeatTime: metav1.NewTime(heartbeat), } }