k3s/pkg/kubelet/nodestatus/setters_test.go

1720 lines
55 KiB
Go

/*
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),
}
}