diff --git a/pkg/resourcemonitor/podresourcesscanner.go b/pkg/resourcemonitor/podresourcesscanner.go index da41d9db3..29b8d2b50 100644 --- a/pkg/resourcemonitor/podresourcesscanner.go +++ b/pkg/resourcemonitor/podresourcesscanner.go @@ -19,11 +19,12 @@ package resourcemonitor import ( "context" "fmt" + "strconv" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/klog/v2" podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1" - v1qos "k8s.io/kubernetes/pkg/apis/core/v1/helper/qos" "sigs.k8s.io/node-feature-discovery/pkg/apihelper" ) @@ -50,50 +51,58 @@ func NewPodResourcesScanner(namespace string, podResourceClient podresourcesapi. } // isWatchable tells if the the given namespace should be watched. -func (resMon *PodResourcesScanner) isWatchable(podNamespace string, podName string) (bool, error) { +func (resMon *PodResourcesScanner) isWatchable(podNamespace string, podName string, hasDevice bool) (bool, bool, error) { cli, err := resMon.apihelper.GetClient() if err != nil { - return false, err + return false, false, err } pod, err := resMon.apihelper.GetPod(cli, podNamespace, podName) if err != nil { - return false, err + return false, false, err } - if v1qos.GetPodQOS(pod) != v1.PodQOSGuaranteed { - return false, nil - } + klog.Infof("podresource: %s", podName) + isIntegralGuaranteed := hasExclusiveCPUs(pod) - if resMon.namespace == "*" && hasExclusiveCPUs(pod) { - return true, nil + if resMon.namespace == "*" && (isIntegralGuaranteed || hasDevice) { + return true, isIntegralGuaranteed, nil } - // TODO: add an explicit check for guaranteed pods - return resMon.namespace == podNamespace && hasExclusiveCPUs(pod), nil + // TODO: add an explicit check for guaranteed pods and pods with devices + return resMon.namespace == podNamespace && (isIntegralGuaranteed || hasDevice), isIntegralGuaranteed, nil } // hasExclusiveCPUs returns true if a guranteed pod is allocated exclusive CPUs else returns false. // In isWatchable() function we check for the pod QoS and proceed if it is guaranteed (i.e. request == limit) // and hence we only check for request in the function below. func hasExclusiveCPUs(pod *v1.Pod) bool { + var totalCPU int64 + var cpuQuantity resource.Quantity for _, container := range pod.Spec.InitContainers { - if _, ok := container.Resources.Requests[v1.ResourceCPU]; !ok { + + var ok bool + if cpuQuantity, ok = container.Resources.Requests[v1.ResourceCPU]; !ok { continue } + totalCPU += cpuQuantity.Value() isInitContainerGuaranteed := hasIntegralCPUs(pod, &container) if !isInitContainerGuaranteed { return false } } for _, container := range pod.Spec.Containers { - if _, ok := container.Resources.Requests[v1.ResourceCPU]; !ok { + var ok bool + if cpuQuantity, ok = container.Resources.Requests[v1.ResourceCPU]; !ok { continue } + totalCPU += cpuQuantity.Value() isAppContainerGuaranteed := hasIntegralCPUs(pod, &container) if !isAppContainerGuaranteed { return false } } - return true + + //No CPUs requested in all the containers in the pod + return totalCPU != 0 } // hasIntegralCPUs returns true if a container in pod is requesting integral CPUs else returns false @@ -116,7 +125,9 @@ func (resMon *PodResourcesScanner) Scan() ([]PodResources, error) { var podResData []PodResources for _, podResource := range resp.GetPodResources() { - isWatchable, err := resMon.isWatchable(podResource.GetNamespace(), podResource.GetName()) + klog.Infof("podresource iter: %s", podResource.GetName()) + hasDevice := hasDevice(podResource) + isWatchable, isIntegralGuaranteed, err := resMon.isWatchable(podResource.GetNamespace(), podResource.GetName(), hasDevice) if err != nil { return nil, fmt.Errorf("checking if pod in a namespace is watchable, namespace:%v, pod name %v: %v", podResource.GetNamespace(), podResource.GetName(), err) } @@ -134,17 +145,19 @@ func (resMon *PodResourcesScanner) Scan() ([]PodResources, error) { Name: container.Name, } - cpuIDs := container.GetCpuIds() - if len(cpuIDs) > 0 { - var resCPUs []string - for _, cpuID := range container.GetCpuIds() { - resCPUs = append(resCPUs, fmt.Sprintf("%d", cpuID)) - } - contRes.Resources = []ResourceInfo{ - { - Name: v1.ResourceCPU, - Data: resCPUs, - }, + if isIntegralGuaranteed { + cpuIDs := container.GetCpuIds() + if len(cpuIDs) > 0 { + var resCPUs []string + for _, cpuID := range container.GetCpuIds() { + resCPUs = append(resCPUs, strconv.FormatInt(cpuID, 10)) + } + contRes.Resources = []ResourceInfo{ + { + Name: v1.ResourceCPU, + Data: resCPUs, + }, + } } } @@ -171,3 +184,13 @@ func (resMon *PodResourcesScanner) Scan() ([]PodResources, error) { return podResData, nil } + +func hasDevice(podResource *podresourcesapi.PodResources) bool { + for _, container := range podResource.GetContainers() { + if len(container.GetDevices()) > 0 { + return true + } + } + klog.Infof("pod:%s doesn't have devices", podResource.GetName()) + return false +} diff --git a/pkg/resourcemonitor/podresourcesscanner_test.go b/pkg/resourcemonitor/podresourcesscanner_test.go index fcf07a9df..a2b1bd08e 100644 --- a/pkg/resourcemonitor/podresourcesscanner_test.go +++ b/pkg/resourcemonitor/podresourcesscanner_test.go @@ -19,15 +19,22 @@ package resourcemonitor import ( "fmt" "reflect" + "sort" "testing" . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/api/resource" v1 "k8s.io/kubelet/pkg/apis/podresources/v1" "sigs.k8s.io/node-feature-discovery/pkg/apihelper" "sigs.k8s.io/node-feature-discovery/pkg/podres" + + corev1 "k8s.io/api/core/v1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sclient "k8s.io/client-go/kubernetes" ) func TestPodScanner(t *testing.T) { @@ -38,6 +45,7 @@ func TestPodScanner(t *testing.T) { Convey("When I scan for pod resources using fake client and no namespace", t, func() { mockPodResClient := new(podres.MockPodResourcesListerClient) mockAPIHelper := new(apihelper.MockAPIHelpers) + mockClient := &k8sclient.Clientset{} resScan, err = NewPodResourcesScanner("*", mockPodResClient, mockAPIHelper) Convey("Creating a Resources Scanner using a mock client", func() { @@ -95,6 +103,37 @@ func TestPodScanner(t *testing.T) { }, } mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) res, err := resScan.Scan() Convey("Error is nil", func() { @@ -124,7 +163,13 @@ func TestPodScanner(t *testing.T) { }, }, } - + for _, podresource := range res { + for _, container := range podresource.Containers { + sort.Slice(res, func(i, j int) bool { + return container.Resources[i].Name < container.Resources[j].Name + }) + } + } So(reflect.DeepEqual(res, expected), ShouldBeTrue) }) }) @@ -151,6 +196,39 @@ func TestPodScanner(t *testing.T) { }, } mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Image: "nginx", + ImagePullPolicy: "Always", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) res, err := resScan.Scan() Convey("Error is nil", func() { @@ -201,6 +279,35 @@ func TestPodScanner(t *testing.T) { }, } mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) res, err := resScan.Scan() Convey("Error is nil", func() { @@ -252,6 +359,37 @@ func TestPodScanner(t *testing.T) { }, } mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Image: "nginx", + ImagePullPolicy: "Always", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) res, err := resScan.Scan() Convey("Error is nil", func() { @@ -282,7 +420,7 @@ func TestPodScanner(t *testing.T) { }) }) - Convey("When I successfully get valid response without resources", func() { + Convey("When I successfully get valid response for (non-guaranteed) pods with devices without cpus", func() { resp := &v1.ListPodResourcesResponse{ PodResources: []*v1.PodResources{ &v1.PodResources{ @@ -291,20 +429,156 @@ func TestPodScanner(t *testing.T) { Containers: []*v1.ContainerResources{ &v1.ContainerResources{ Name: "test-cnt-0", + Devices: []*v1.ContainerDevices{ + &v1.ContainerDevices{ + ResourceName: "fake.io/resource", + DeviceIds: []string{"devA"}, + }, + }, }, }, }, }, } mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) res, err := resScan.Scan() Convey("Error is nil", func() { So(err, ShouldBeNil) }) - Convey("Return PodResources should not have values", func() { - So(len(res), ShouldEqual, 0) + Convey("Return PodResources should have values", func() { + So(len(res), ShouldBeGreaterThan, 0) }) + + expected := []PodResources{ + PodResources{ + Name: "test-pod-0", + Namespace: "default", + Containers: []ContainerResources{ + ContainerResources{ + Name: "test-cnt-0", + Resources: []ResourceInfo{ + ResourceInfo{ + Name: "fake.io/resource", + Data: []string{"devA"}, + }, + }, + }, + }, + }, + } + + So(reflect.DeepEqual(res, expected), ShouldBeTrue) + }) + + Convey("When I successfully get valid response for (non-guaranteed) pods with devices with cpus", func() { + resp := &v1.ListPodResourcesResponse{ + PodResources: []*v1.PodResources{ + &v1.PodResources{ + Name: "test-pod-0", + Namespace: "default", + Containers: []*v1.ContainerResources{ + &v1.ContainerResources{ + Name: "test-cnt-0", + CpuIds: []int64{0, 1}, + Devices: []*v1.ContainerDevices{ + &v1.ContainerDevices{ + ResourceName: "fake.io/resource", + DeviceIds: []string{"devA"}, + }, + }, + }, + }, + }, + }, + } + mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + corev1.ResourceCPU: resource.MustParse("1500m"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + corev1.ResourceCPU: resource.MustParse("1500m"), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) + res, err := resScan.Scan() + + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + Convey("Return PodResources should have values", func() { + So(len(res), ShouldBeGreaterThan, 0) + }) + + expected := []PodResources{ + PodResources{ + Name: "test-pod-0", + Namespace: "default", + Containers: []ContainerResources{ + ContainerResources{ + Name: "test-cnt-0", + Resources: []ResourceInfo{ + ResourceInfo{ + Name: "fake.io/resource", + Data: []string{"devA"}, + }, + }, + }, + }, + }, + } + So(reflect.DeepEqual(res, expected), ShouldBeTrue) }) }) @@ -312,6 +586,7 @@ func TestPodScanner(t *testing.T) { Convey("When I scan for pod resources using fake client and given namespace", t, func() { mockPodResClient := new(podres.MockPodResourcesListerClient) mockAPIHelper := new(apihelper.MockAPIHelpers) + mockClient := &k8sclient.Clientset{} resScan, err = NewPodResourcesScanner("pod-res-test", mockPodResClient, mockAPIHelper) Convey("Creating a Resources Scanner using a mock client", func() { @@ -369,6 +644,37 @@ func TestPodScanner(t *testing.T) { }, } mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) res, err := resScan.Scan() Convey("Error is nil", func() { @@ -379,6 +685,377 @@ func TestPodScanner(t *testing.T) { }) }) - }) + Convey("When I successfully get valid response when pod is in the monitoring namespace", func() { + resp := &v1.ListPodResourcesResponse{ + PodResources: []*v1.PodResources{ + &v1.PodResources{ + Name: "test-pod-0", + Namespace: "pod-res-test", + Containers: []*v1.ContainerResources{ + &v1.ContainerResources{ + Name: "test-cnt-0", + Devices: []*v1.ContainerDevices{ + &v1.ContainerDevices{ + ResourceName: "fake.io/resource", + DeviceIds: []string{"devA"}, + }, + }, + CpuIds: []int64{0, 1}, + }, + }, + }, + }, + } + mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "pod-res-test", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Image: "nginx", + ImagePullPolicy: "Always", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "pod-res-test", "test-pod-0").Return(pod, nil) + res, err := resScan.Scan() + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + + Convey("Return PodResources should have values", func() { + So(len(res), ShouldBeGreaterThan, 0) + + expected := []PodResources{ + PodResources{ + Name: "test-pod-0", + Namespace: "pod-res-test", + Containers: []ContainerResources{ + ContainerResources{ + Name: "test-cnt-0", + Resources: []ResourceInfo{ + ResourceInfo{ + Name: "cpu", + Data: []string{"0", "1"}, + }, + ResourceInfo{ + Name: "fake.io/resource", + Data: []string{"devA"}, + }, + }, + }, + }, + }, + } + + So(reflect.DeepEqual(res, expected), ShouldBeTrue) + }) + }) + + Convey("When I successfully get valid empty response for a pod not in the monitoring namespace without devices", func() { + resp := &v1.ListPodResourcesResponse{ + PodResources: []*v1.PodResources{ + &v1.PodResources{ + Name: "test-pod-0", + Namespace: "default", + Containers: []*v1.ContainerResources{ + &v1.ContainerResources{ + Name: "test-cnt-0", + CpuIds: []int64{0, 1}, + }, + }, + }, + }, + } + mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) + res, err := resScan.Scan() + + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + Convey("Return PodResources should have values", func() { + Convey("Return PodResources should be zero", func() { + So(len(res), ShouldEqual, 0) + }) + }) + }) + + Convey("When I successfully get an empty valid response for a pod without cpus when pod is not in the monitoring namespace", func() { + resp := &v1.ListPodResourcesResponse{ + PodResources: []*v1.PodResources{ + &v1.PodResources{ + Name: "test-pod-0", + Namespace: "default", + Containers: []*v1.ContainerResources{ + &v1.ContainerResources{ + Name: "test-cnt-0", + Devices: []*v1.ContainerDevices{ + &v1.ContainerDevices{ + ResourceName: "fake.io/resource", + DeviceIds: []string{"devA"}, + }, + }, + }, + }, + }, + }, + } + mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Image: "nginx", + ImagePullPolicy: "Always", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "default", "test-pod-0").Return(pod, nil) + res, err := resScan.Scan() + + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + Convey("Return PodResources should have values", func() { + So(len(res), ShouldEqual, 0) + }) + }) + + Convey("When I successfully get valid response for (non-guaranteed) pods with devices without cpus", func() { + resp := &v1.ListPodResourcesResponse{ + PodResources: []*v1.PodResources{ + &v1.PodResources{ + Name: "test-pod-0", + Namespace: "pod-res-test", + Containers: []*v1.ContainerResources{ + &v1.ContainerResources{ + Name: "test-cnt-0", + Devices: []*v1.ContainerDevices{ + &v1.ContainerDevices{ + ResourceName: "fake.io/resource", + DeviceIds: []string{"devA"}, + }, + }, + }, + }, + }, + }, + } + mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "pod-res-test", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + }, + Limits: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "pod-res-test", "test-pod-0").Return(pod, nil) + res, err := resScan.Scan() + + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + Convey("Return PodResources should have values", func() { + So(len(res), ShouldBeGreaterThan, 0) + }) + + expected := []PodResources{ + PodResources{ + Name: "test-pod-0", + Namespace: "pod-res-test", + Containers: []ContainerResources{ + ContainerResources{ + Name: "test-cnt-0", + Resources: []ResourceInfo{ + ResourceInfo{ + Name: "fake.io/resource", + Data: []string{"devA"}, + }, + }, + }, + }, + }, + } + + So(reflect.DeepEqual(res, expected), ShouldBeTrue) + }) + + Convey("When I successfully get valid response for (non-guaranteed) pods with devices with cpus", func() { + resp := &v1.ListPodResourcesResponse{ + PodResources: []*v1.PodResources{ + &v1.PodResources{ + Name: "test-pod-0", + Namespace: "pod-res-test", + Containers: []*v1.ContainerResources{ + &v1.ContainerResources{ + Name: "test-cnt-0", + CpuIds: []int64{0, 1}, + Devices: []*v1.ContainerDevices{ + &v1.ContainerDevices{ + ResourceName: "fake.io/resource", + DeviceIds: []string{"devA"}, + }, + }, + }, + }, + }, + }, + } + mockPodResClient.On("List", mock.AnythingOfType("*context.timerCtx"), mock.AnythingOfType("*v1.ListPodResourcesRequest")).Return(resp, nil) + pod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod-0", + Namespace: "pod-res-test", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-cnt-0", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + corev1.ResourceCPU: resource.MustParse("1500m"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceName("fake.io/resource"): *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), + corev1.ResourceCPU: resource.MustParse("1500m"), + }, + }, + }, + }, + }, + } + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetPod", mock.AnythingOfType("*kubernetes.Clientset"), "pod-res-test", "test-pod-0").Return(pod, nil) + res, err := resScan.Scan() + + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + Convey("Return PodResources should have values", func() { + So(len(res), ShouldBeGreaterThan, 0) + }) + + expected := []PodResources{ + PodResources{ + Name: "test-pod-0", + Namespace: "pod-res-test", + Containers: []ContainerResources{ + ContainerResources{ + Name: "test-cnt-0", + Resources: []ResourceInfo{ + ResourceInfo{ + Name: "fake.io/resource", + Data: []string{"devA"}, + }, + }, + }, + }, + }, + } + So(reflect.DeepEqual(res, expected), ShouldBeTrue) + }) + + }) }