diff --git a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go index 17091d92491..afd56352de9 100644 --- a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go +++ b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go @@ -32,7 +32,7 @@ var ( v1.ResourceCPU, v1.ResourceMemory, } - NodeColumns = []string{"NAME", "CPU(cores)", "CPU%", "MEMORY(bytes)", "MEMORY%"} + NodeColumns = []string{"NAME", "CPU(cores)", "CPU(%)", "MEMORY(bytes)", "MEMORY(%)"} PodColumns = []string{"NAME", "CPU(cores)", "MEMORY(bytes)"} NamespaceColumn = "NAMESPACE" PodColumn = "POD" diff --git a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer_test.go b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer_test.go new file mode 100644 index 00000000000..a2983f83e5f --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer_test.go @@ -0,0 +1,396 @@ +/* +Copyright 2024 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 metricsutil + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericiooptions" + metricsapi "k8s.io/metrics/pkg/apis/metrics" +) + +func TestPrintNodeMetrics(t *testing.T) { + + tests := []struct { + name string + nodeMetric []metricsapi.NodeMetrics + nodes []*v1.Node + noHeader bool + sortBy string + expectedErr error + expectedOutput string + }{ + { + name: "Single node with default header", + nodes: []*v1.Node{newNode("node1")}, + nodeMetric: []metricsapi.NodeMetrics{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, + Window: metav1.Duration{Duration: time.Minute}, + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + expectedOutput: `NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%) +node1 1000m 10% 1024Mi 10% +`, + }, + { + name: "Single node without header", + nodes: []*v1.Node{newNode("node1")}, + nodeMetric: []metricsapi.NodeMetrics{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, + Window: metav1.Duration{Duration: time.Minute}, + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + noHeader: true, + expectedOutput: `node1 1000m 10% 1024Mi 10% +`, + }, + { + name: "Multiple nodes with one missing metrics", + nodes: []*v1.Node{newNode("node1"), newNode("node2")}, + nodeMetric: []metricsapi.NodeMetrics{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, + Window: metav1.Duration{Duration: time.Minute}, + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + expectedOutput: `NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%) +node1 1000m 10% 1024Mi 10% +node2 +`, + }, + { + name: "Multiple nodes with metrics sorted by memory", + nodes: []*v1.Node{newNode("node1"), newNode("node2")}, + nodeMetric: []metricsapi.NodeMetrics{ + { + ObjectMeta: metav1.ObjectMeta{Name: "node1", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, + Window: metav1.Duration{Duration: time.Minute}, + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "node2", ResourceVersion: "10", Labels: map[string]string{"key": "value"}}, + Window: metav1.Duration{Duration: time.Minute}, + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("2"), + v1.ResourceMemory: resource.MustParse("5Gi"), + }, + }, + }, + sortBy: "memory", + expectedOutput: `NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%) +node2 2000m 20% 5120Mi 50% +node1 1000m 10% 1024Mi 10% +`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create a new TopCmdPrinter with a test writer. + _, _, out, _ := genericiooptions.NewTestIOStreams() + + availableResources := make(map[string]v1.ResourceList) + for _, n := range test.nodes { + availableResources[n.Name] = n.Status.Capacity + } + top := NewTopCmdPrinter(out) + err := top.PrintNodeMetrics(test.nodeMetric, availableResources, test.noHeader, test.sortBy) + assert.Equal(t, test.expectedErr, err) + assert.Equal(t, test.expectedOutput, out.String()) + }) + } +} + +func TestPrintPodMetrics(t *testing.T) { + + tests := []struct { + name string + podMetric []metricsapi.PodMetrics + printContainers bool + withNamespace bool + noHeader bool + sortBy string + sum bool + expectedErr error + expectedOutput string + }{ + { + name: "Single Pod with Multiple Containers", + podMetric: []metricsapi.PodMetrics{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + }, + expectedOutput: `NAME CPU(cores) MEMORY(bytes) +test 400m 2048Mi +`, + }, + { + name: "Single Pod with Multiple Containers - Print Containers", + podMetric: []metricsapi.PodMetrics{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + }, + printContainers: true, + expectedOutput: `POD NAME CPU(cores) MEMORY(bytes) +test container1 200m 1024Mi +test container2 200m 1024Mi +`, + }, + { + name: "Single Pod with Multiple Containers - Calculate Resource Usage Without Header", + podMetric: []metricsapi.PodMetrics{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + }, + noHeader: true, + expectedOutput: `test 400m 2048Mi +`, + }, + { + name: "Multiple Pods with Multiple Containers - Sort by Memory Usage", + podMetric: []metricsapi.PodMetrics{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-1", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("3Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + }, + }, + sortBy: "memory", + expectedOutput: `NAME CPU(cores) MEMORY(bytes) +test-1 400m 5120Mi +test 400m 2048Mi +`, + }, + { + name: "Multiple Pods with Multiple Containers - Calculate Total Resource Usage", + podMetric: []metricsapi.PodMetrics{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-1", + Namespace: "default", + }, + Timestamp: metav1.Time{Time: time.Now()}, + Window: metav1.Duration{Duration: time.Minute}, + Containers: []metricsapi.ContainerMetrics{ + { + Name: "container1", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("3Gi"), + }, + }, + { + Name: "container2", + Usage: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("0.2"), + v1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + }, + }, + sum: true, + expectedOutput: `NAME CPU(cores) MEMORY(bytes) +test 400m 2048Mi +test-1 400m 5120Mi + ________ ________ + 800m 7168Mi +`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create a new TopCmdPrinter with a test writer. + _, _, out, _ := genericiooptions.NewTestIOStreams() + + top := NewTopCmdPrinter(out) + err := top.PrintPodMetrics(test.podMetric, test.printContainers, + test.withNamespace, test.noHeader, test.sortBy, test.sum) + assert.Equal(t, test.expectedErr, err) + assert.Equal(t, test.expectedOutput, out.String()) + }) + } +} + +func newNode(name string) *v1.Node { + return &v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Status: v1.NodeStatus{ + Capacity: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), + v1.ResourceName(v1.ResourceMemory): resource.MustParse("10Gi"), + }, + }, + } +}