From 8d3fb9ee0a51b6a6ea135d991391c35806422c19 Mon Sep 17 00:00:00 2001 From: Itamar Holder <77444623+iholder101@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:10:38 +0300 Subject: [PATCH] [KEP-2400] kubectl top: add a --show-swap option (#129458) * top, refactor: turn package-exposed variables to unexpose struct fields Signed-off-by: Itamar Holder * kubectl top node: add the --show-swap option Example output: > kubectl top node --show-swap NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%) SWAP(bytes) SWAP(%) node01 500m 8% 2836Mi 60% 0Mi 0% node02 260m 5% 2206Mi 47% 512Mi 50% Signed-off-by: Itamar Holder * kubectl top pod: add the --show-swap option Example output: > kubectl top pod -n kube-system --show-swap NAME CPU(cores) MEMORY(bytes) SWAP(bytes) coredns-58d5bc5cdb-5nbk4 2m 19Mi 0Mi coredns-58d5bc5cdb-jsh26 3m 37Mi 0Mi etcd-node01 51m 143Mi 0Mi kube-apiserver-node01 98m 824Mi 0Mi kube-controller-manager-node01 20m 135Mi 0Mi kube-proxy-ffgs2 1m 24Mi 0Mi kube-proxy-fhvwx 1m 39Mi 0Mi kube-scheduler-node01 13m 69Mi 0Mi metrics-server-8598789fdb-d2kcj 5m 26Mi 0Mi Signed-off-by: Itamar Holder * kubectl top node --show-swap: add unit tests Signed-off-by: Itamar Holder * kubectl top pod --show-swap: Add unit tests Signed-off-by: Itamar Holder * Explicitly mark swap as unavailable when necessary Signed-off-by: Itamar Holder --------- Signed-off-by: Itamar Holder --- .../k8s.io/kubectl/pkg/cmd/top/top_node.go | 14 +- .../kubectl/pkg/cmd/top/top_node_test.go | 108 ++++++++++++++++ .../src/k8s.io/kubectl/pkg/cmd/top/top_pod.go | 4 +- .../kubectl/pkg/cmd/top/top_pod_test.go | 100 ++++++++++++++ .../k8s.io/kubectl/pkg/cmd/top/top_test.go | 19 +++ .../pkg/metricsutil/metrics_printer.go | 122 ++++++++++++------ .../pkg/metricsutil/metrics_printer_test.go | 4 +- .../kubectl/pkg/metricsutil/metrics_sorter.go | 4 +- 8 files changed, 327 insertions(+), 48 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node.go b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node.go index b6b36e4d1d3..f0a1a389782 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node.go @@ -19,9 +19,9 @@ package top import ( "context" "errors" - "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/cli-runtime/pkg/genericiooptions" @@ -45,6 +45,7 @@ type TopNodeOptions struct { NoHeaders bool UseProtocolBuffers bool ShowCapacity bool + ShowSwap bool NodeClient corev1client.CoreV1Interface Printer *metricsutil.TopCmdPrinter @@ -95,6 +96,7 @@ func NewCmdTopNode(f cmdutil.Factory, o *TopNodeOptions, streams genericiooption cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers") cmd.Flags().BoolVar(&o.UseProtocolBuffers, "use-protocol-buffers", o.UseProtocolBuffers, "Enables using protocol-buffers to access Metrics API.") cmd.Flags().BoolVar(&o.ShowCapacity, "show-capacity", o.ShowCapacity, "Print node resources based on Capacity instead of Allocatable(default) of the nodes.") + cmd.Flags().BoolVar(&o.ShowSwap, "show-swap", o.ShowSwap, "Print node resources related to swap memory.") return cmd } @@ -127,7 +129,7 @@ func (o *TopNodeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args [] o.NodeClient = clientset.CoreV1() - o.Printer = metricsutil.NewTopCmdPrinter(o.Out) + o.Printer = metricsutil.NewTopCmdPrinter(o.Out, o.ShowSwap) return nil } @@ -198,6 +200,14 @@ func (o TopNodeOptions) RunTopNode() error { } else { availableResources[n.Name] = n.Status.Capacity } + + if n.Status.NodeInfo.Swap != nil && n.Status.NodeInfo.Swap.Capacity != nil { + swapCapacity := *n.Status.NodeInfo.Swap.Capacity + availableResources[n.Name]["swap"] = *resource.NewQuantity(swapCapacity, resource.BinarySI) + } else { + o.Printer.RegisterMissingResource(n.Name, metricsutil.ResourceSwap) + } + } return o.Printer.PrintNodeMetrics(metrics.Items, availableResources, o.NoHeaders, o.SortBy) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node_test.go index db43152dcd0..6b559c87095 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_node_test.go @@ -424,3 +424,111 @@ func TestTopNodeWithSortByMemoryMetricsFrom(t *testing.T) { } } + +func TestTopNodeWithSwap(t *testing.T) { + runTopCmd := func(expectedMetrics *metricsv1beta1api.NodeMetricsList, nodes *v1.NodeList) (result string) { + cmdtesting.InitTestErrorHandler(t) + expectedNodePath := fmt.Sprintf("/%s/%s/nodes", apiPrefix, apiVersion) + + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + ns := scheme.Codecs.WithoutConversion() + + tf.Client = &fake.RESTClient{ + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == "/api": + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil + case p == "/apis": + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil + case p == expectedNodePath && m == "GET": + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, nodes)}, nil + default: + t.Fatalf("unexpected request: %#v\nGot URL: %#v\n", req, req.URL) + return nil, nil + } + }), + } + fakemetricsClientset := &metricsfake.Clientset{} + fakemetricsClientset.AddReactor("list", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { + return true, expectedMetrics, nil + }) + tf.ClientConfigVal = cmdtesting.DefaultClientConfig() + streams, _, buf, _ := genericiooptions.NewTestIOStreams() + + cmd := NewCmdTopNode(tf, nil, streams) + + // TODO in the long run, we want to test most of our commands like this. Wire the options struct with specific mocks + // TODO then check the particular Run functionality and harvest results from fake clients + cmdOptions := &TopNodeOptions{ + IOStreams: streams, + ShowSwap: true, + } + if err := cmdOptions.Complete(tf, cmd, []string{}); err != nil { + t.Fatal(err) + } + cmdOptions.MetricsClient = fakemetricsClientset + if err := cmdOptions.Validate(); err != nil { + t.Fatal(err) + } + if err := cmdOptions.RunTopNode(); err != nil { + t.Fatal(err) + } + + return buf.String() + } + + for _, tc := range []struct { + name string + isSwapDisabledOnNodes bool + }{ + { + name: "nodes with swap", + isSwapDisabledOnNodes: false, + }, + { + name: "nodes without swap", + isSwapDisabledOnNodes: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + expectedMetrics, nodes := testNodeV1beta1MetricsData() + if tc.isSwapDisabledOnNodes { + for i := range expectedMetrics.Items { + delete(expectedMetrics.Items[i].Usage, "swap") + } + for i := range nodes.Items { + nodes.Items[i].Status.NodeInfo.Swap = nil + } + } + + result := runTopCmd(expectedMetrics, nodes) + fmt.Printf("%s\n", result) + + if !strings.Contains(result, "SWAP(bytes)") { + t.Errorf("missing SWAP(bytes) header: \n%s", result) + } + if !strings.Contains(result, "SWAP(%)") { + t.Errorf("missing SWAP(%%) header: \n%s", result) + } + + if tc.isSwapDisabledOnNodes { + if !strings.Contains(result, "") { + t.Errorf("expected swap to be : \n%s", result) + } + } + + for _, m := range expectedMetrics.Items { + if !strings.Contains(result, m.Name) { + t.Errorf("missing metrics for %s: \n%s", m.Name, result) + } + if _, foundSwapMetric := m.Usage["swap"]; foundSwapMetric != !tc.isSwapDisabledOnNodes { + t.Errorf("missing swap metric for %s: \n%s", m.Name, result) + } + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go index 62a59855304..c19cc4a801f 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go @@ -53,6 +53,7 @@ type TopPodOptions struct { NoHeaders bool UseProtocolBuffers bool Sum bool + ShowSwap bool PodClient corev1client.PodsGetter Printer *metricsutil.TopCmdPrinter @@ -117,6 +118,7 @@ func NewCmdTopPod(f cmdutil.Factory, o *TopPodOptions, streams genericiooptions. cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers.") cmd.Flags().BoolVar(&o.UseProtocolBuffers, "use-protocol-buffers", o.UseProtocolBuffers, "Enables using protocol-buffers to access Metrics API.") cmd.Flags().BoolVar(&o.Sum, "sum", o.Sum, "Print the sum of the resource usage") + cmd.Flags().BoolVar(&o.ShowSwap, "show-swap", o.ShowSwap, "Print pod resources related to swap memory.") return cmd } @@ -152,7 +154,7 @@ func (o *TopPodOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []s o.PodClient = clientset.CoreV1() - o.Printer = metricsutil.NewTopCmdPrinter(o.Out) + o.Printer = metricsutil.NewTopCmdPrinter(o.Out, o.ShowSwap) return nil } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod_test.go index 87d36f78df1..d2e4d65faa0 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod_test.go @@ -168,6 +168,12 @@ func TestTopPod(t *testing.T) { namespaces: []string{testNS, testNS, testNS}, containers: true, }, + { + name: "with swap", + options: &TopPodOptions{AllNamespaces: true, ShowSwap: true}, + namespaces: []string{testNS, "secondtestns", "thirdtestns"}, + listsNamespaces: true, + }, } cmdtesting.InitTestErrorHandler(t) for _, testCase := range testCases { @@ -284,10 +290,101 @@ func TestTopPod(t *testing.T) { t.Errorf("containers not matching:\n\texpectedContainers: %v\n\tresultContainers: %v\n", testCase.expectedContainers, resultContainers) } } + if testCase.options != nil && testCase.options.ShowSwap { + if !strings.Contains(result, "SWAP(bytes)") { + t.Errorf("missing SWAP(bytes) header: \n%s", result) + } + } }) } } +func TestTopPodWithSwap(t *testing.T) { + cmdtesting.InitTestErrorHandler(t) + + const testName = "TestTopPodWithSwap" + t.Run(testName, func(t *testing.T) { + metricsList := testV1beta1PodMetricsData() + fakemetricsClientset := &metricsfake.Clientset{} + + fakemetricsClientset.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { + res := &metricsv1beta1api.PodMetricsList{ + Items: metricsList, + } + return true, res, nil + }) + + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + + ns := scheme.Codecs.WithoutConversion() + + tf.Client = &fake.RESTClient{ + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case "/api": + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apibody)))}, nil + case "/apis": + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: io.NopCloser(bytes.NewReader([]byte(apisbodyWithMetrics)))}, nil + default: + t.Fatalf("%s: unexpected request: %#v\nGot URL: %#v", + testName, req, req.URL) + return nil, nil + } + }), + } + streams, _, buf, _ := genericiooptions.NewTestIOStreams() + + cmd := NewCmdTopPod(tf, nil, streams) + cmdOptions := &TopPodOptions{ + ShowSwap: true, + } + cmdOptions.IOStreams = streams + + if err := cmdOptions.Complete(tf, cmd, nil); err != nil { + t.Fatal(err) + } + cmdOptions.MetricsClient = fakemetricsClientset + if err := cmdOptions.Validate(); err != nil { + t.Fatal(err) + } + if err := cmdOptions.RunTopPod(); err != nil { + t.Fatal(err) + } + + result := buf.String() + + expectedSwapBytes := map[string]string{ + "pod1": "4Mi", + "pod2": "0Mi", + "pod3": "3Mi", + } + + actualSwapBytes := map[string]string{} + for _, line := range strings.Split(result, "\n")[1:] { + lineFields := strings.Fields(line) + if len(lineFields) < 4 { + continue + } + + podName := lineFields[0] + swapBytes := lineFields[3] + actualSwapBytes[podName] = swapBytes + } + + for expectedPodName, expectedSwapBytes := range expectedSwapBytes { + actualSwapBytes, found := actualSwapBytes[expectedPodName] + if !found { + t.Errorf("missing swap metrics for pod %s", expectedPodName) + } + if actualSwapBytes != expectedSwapBytes { + t.Errorf("unexpected swap metrics for pod %s: expected %s, got %s", expectedPodName, expectedSwapBytes, actualSwapBytes) + } + } + }) +} + func getResultColumnValues(result string, columnIndex int) []string { resultLines := strings.Split(result, "\n") values := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line @@ -410,6 +507,7 @@ func testV1beta1PodMetricsData() []metricsv1beta1api.PodMetrics { Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), + "swap": *resource.NewQuantity(1*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), }, }, @@ -418,6 +516,7 @@ func testV1beta1PodMetricsData() []metricsv1beta1api.PodMetrics { Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(4, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(5*(1024*1024), resource.DecimalSI), + "swap": *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI), }, }, @@ -462,6 +561,7 @@ func testV1beta1PodMetricsData() []metricsv1beta1api.PodMetrics { Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(7, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(8*(1024*1024), resource.DecimalSI), + "swap": *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(9*(1024*1024), resource.DecimalSI), }, }, diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_test.go index db511c39deb..6e8a1f02508 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/top/top_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/top/top_test.go @@ -30,6 +30,7 @@ import ( "k8s.io/cli-runtime/pkg/genericiooptions" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" + "k8s.io/utils/ptr" ) func TestTopSubcommandsExist(t *testing.T) { @@ -63,6 +64,7 @@ func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeL Window: metav1.Duration{Duration: time.Minute}, Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(1, resource.DecimalSI), + "swap": *resource.NewQuantity(1*(1024*1024), resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(3*(1024*1024), resource.DecimalSI), }, @@ -72,6 +74,7 @@ func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeL Window: metav1.Duration{Duration: time.Minute}, Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(5, resource.DecimalSI), + "swap": *resource.NewQuantity(2*(1024*1024), resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(6*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(7*(1024*1024), resource.DecimalSI), }, @@ -81,6 +84,7 @@ func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeL Window: metav1.Duration{Duration: time.Minute}, Usage: v1.ResourceList{ v1.ResourceCPU: *resource.NewMilliQuantity(3, resource.DecimalSI), + "swap": *resource.NewQuantity(0, resource.DecimalSI), v1.ResourceMemory: *resource.NewQuantity(4*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(5*(1024*1024), resource.DecimalSI), }, @@ -100,6 +104,11 @@ func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeL v1.ResourceMemory: *resource.NewQuantity(20*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(30*(1024*1024), resource.DecimalSI), }, + NodeInfo: v1.NodeSystemInfo{ + Swap: &v1.NodeSwapStatus{ + Capacity: ptr.To(int64(10 * (1024 * 1024 * 1024))), + }, + }, }, }, { @@ -110,6 +119,11 @@ func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeL v1.ResourceMemory: *resource.NewQuantity(60*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(70*(1024*1024), resource.DecimalSI), }, + NodeInfo: v1.NodeSystemInfo{ + Swap: &v1.NodeSwapStatus{ + Capacity: ptr.To(int64(20 * (1024 * 1024 * 1024))), + }, + }, }, }, { @@ -120,6 +134,11 @@ func testNodeV1beta1MetricsData() (*metricsv1beta1api.NodeMetricsList, *v1.NodeL v1.ResourceMemory: *resource.NewQuantity(40*(1024*1024), resource.DecimalSI), v1.ResourceStorage: *resource.NewQuantity(50*(1024*1024), resource.DecimalSI), }, + NodeInfo: v1.NodeSystemInfo{ + Swap: &v1.NodeSwapStatus{ + Capacity: ptr.To(int64(30 * (1024 * 1024 * 1024))), + }, + }, }, }, }, 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 afd56352de9..9c7fb5b869b 100644 --- a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go +++ b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go @@ -19,6 +19,7 @@ package metricsutil import ( "fmt" "io" + "slices" "sort" "k8s.io/api/core/v1" @@ -28,16 +29,12 @@ import ( ) var ( - MeasuredResources = []v1.ResourceName{ - v1.ResourceCPU, - v1.ResourceMemory, - } - NodeColumns = []string{"NAME", "CPU(cores)", "CPU(%)", "MEMORY(bytes)", "MEMORY(%)"} - PodColumns = []string{"NAME", "CPU(cores)", "MEMORY(bytes)"} NamespaceColumn = "NAMESPACE" PodColumn = "POD" ) +const ResourceSwap = "swap" + type ResourceMetricsInfo struct { Name string Metrics v1.ResourceList @@ -45,11 +42,33 @@ type ResourceMetricsInfo struct { } type TopCmdPrinter struct { - out io.Writer + out io.Writer + measuredResources []v1.ResourceName + nodeColumns []string + podColumns []string + // a map from a node name to its missing resources + nodesMissingResources map[string][]string } -func NewTopCmdPrinter(out io.Writer) *TopCmdPrinter { - return &TopCmdPrinter{out: out} +func NewTopCmdPrinter(out io.Writer, showSwap bool) *TopCmdPrinter { + printer := &TopCmdPrinter{ + out: out, + measuredResources: []v1.ResourceName{ + v1.ResourceCPU, + v1.ResourceMemory, + }, + nodeColumns: []string{"NAME", "CPU(cores)", "CPU(%)", "MEMORY(bytes)", "MEMORY(%)"}, + podColumns: []string{"NAME", "CPU(cores)", "MEMORY(bytes)"}, + nodesMissingResources: make(map[string][]string), + } + + if showSwap { + printer.measuredResources = append(printer.measuredResources, ResourceSwap) + printer.nodeColumns = append(printer.nodeColumns, "SWAP(bytes)", "SWAP(%)") + printer.podColumns = append(printer.podColumns, "SWAP(bytes)") + } + + return printer } func (printer *TopCmdPrinter) PrintNodeMetrics(metrics []metricsapi.NodeMetrics, availableResources map[string]v1.ResourceList, noHeaders bool, sortBy string) error { @@ -59,25 +78,26 @@ func (printer *TopCmdPrinter) PrintNodeMetrics(metrics []metricsapi.NodeMetrics, w := printers.GetNewTabWriter(printer.out) defer w.Flush() + measuredResources := printer.measuredResources sort.Sort(NewNodeMetricsSorter(metrics, sortBy)) if !noHeaders { - printColumnNames(w, NodeColumns) + printColumnNames(w, printer.nodeColumns) } var usage v1.ResourceList for _, m := range metrics { m.Usage.DeepCopyInto(&usage) - printMetricsLine(w, &ResourceMetricsInfo{ + printer.printMetricsLine(w, &ResourceMetricsInfo{ Name: m.Name, Metrics: usage, Available: availableResources[m.Name], - }) + }, measuredResources) delete(availableResources, m.Name) } // print lines for nodes of which the metrics is unreachable. for nodeName := range availableResources { - printMissingMetricsNodeLine(w, nodeName) + printer.printMissingMetricsNodeLine(w, nodeName, measuredResources) } return nil } @@ -89,7 +109,7 @@ func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, p w := printers.GetNewTabWriter(printer.out) defer w.Flush() - columnWidth := len(PodColumns) + columnWidth := len(printer.podColumns) if !noHeaders { if withNamespace { printValue(w, NamespaceColumn) @@ -99,32 +119,39 @@ func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, p printValue(w, PodColumn) columnWidth++ } - printColumnNames(w, PodColumns) + printColumnNames(w, printer.podColumns) } - sort.Sort(NewPodMetricsSorter(metrics, withNamespace, sortBy)) + sort.Sort(NewPodMetricsSorter(metrics, withNamespace, sortBy, printer.measuredResources)) for _, m := range metrics { if printContainers { sort.Sort(NewContainerMetricsSorter(m.Containers, sortBy)) - printSinglePodContainerMetrics(w, &m, withNamespace) + printer.printSinglePodContainerMetrics(w, &m, withNamespace, printer.measuredResources) } else { - printSinglePodMetrics(w, &m, withNamespace) + printer.printSinglePodMetrics(w, &m, withNamespace, printer.measuredResources) } } if sum { - adder := NewResourceAdder(MeasuredResources) + adder := NewResourceAdder(printer.measuredResources) for _, m := range metrics { adder.AddPodMetrics(&m) } - printPodResourcesSum(w, adder.total, columnWidth) + printer.printPodResourcesSum(w, adder.total, columnWidth, printer.measuredResources) } return nil } +func (printer *TopCmdPrinter) RegisterMissingResource(nodeName, resourceName string) { + if slices.Contains(printer.nodesMissingResources[nodeName], resourceName) { + return + } + printer.nodesMissingResources[nodeName] = append(printer.nodesMissingResources[nodeName], resourceName) +} + func printColumnNames(out io.Writer, names []string) { for _, name := range names { printValue(out, name) @@ -132,40 +159,40 @@ func printColumnNames(out io.Writer, names []string) { fmt.Fprint(out, "\n") } -func printSinglePodMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool) { - podMetrics := getPodMetrics(m) +func (printer *TopCmdPrinter) printSinglePodMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool, measuredResources []v1.ResourceName) { + podMetrics := getPodMetrics(m, measuredResources) if withNamespace { printValue(out, m.Namespace) } - printMetricsLine(out, &ResourceMetricsInfo{ + printer.printMetricsLine(out, &ResourceMetricsInfo{ Name: m.Name, Metrics: podMetrics, Available: v1.ResourceList{}, - }) + }, measuredResources) } -func printSinglePodContainerMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool) { +func (printer *TopCmdPrinter) printSinglePodContainerMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool, measuredResources []v1.ResourceName) { for _, c := range m.Containers { if withNamespace { printValue(out, m.Namespace) } printValue(out, m.Name) - printMetricsLine(out, &ResourceMetricsInfo{ + printer.printMetricsLine(out, &ResourceMetricsInfo{ Name: c.Name, Metrics: c.Usage, Available: v1.ResourceList{}, - }) + }, measuredResources) } } -func getPodMetrics(m *metricsapi.PodMetrics) v1.ResourceList { +func getPodMetrics(m *metricsapi.PodMetrics, measuredResources []v1.ResourceName) v1.ResourceList { podMetrics := make(v1.ResourceList) - for _, res := range MeasuredResources { + for _, res := range measuredResources { podMetrics[res], _ = resource.ParseQuantity("0") } for _, c := range m.Containers { - for _, res := range MeasuredResources { + for _, res := range measuredResources { quantity := podMetrics[res] quantity.Add(c.Usage[res]) podMetrics[res] = quantity @@ -174,16 +201,16 @@ func getPodMetrics(m *metricsapi.PodMetrics) v1.ResourceList { return podMetrics } -func printMetricsLine(out io.Writer, metrics *ResourceMetricsInfo) { +func (printer *TopCmdPrinter) printMetricsLine(out io.Writer, metrics *ResourceMetricsInfo, measuredResources []v1.ResourceName) { printValue(out, metrics.Name) - printAllResourceUsages(out, metrics) + printer.printAllResourceUsages(out, metrics, measuredResources) fmt.Fprint(out, "\n") } -func printMissingMetricsNodeLine(out io.Writer, nodeName string) { +func (printer *TopCmdPrinter) printMissingMetricsNodeLine(out io.Writer, nodeName string, measuredResources []v1.ResourceName) { printValue(out, nodeName) unknownMetricsStatus := "" - for i := 0; i < len(MeasuredResources); i++ { + for i := 0; i < len(measuredResources); i++ { printValue(out, unknownMetricsStatus) printValue(out, unknownMetricsStatus) } @@ -194,13 +221,21 @@ func printValue(out io.Writer, value interface{}) { fmt.Fprintf(out, "%v\t", value) } -func printAllResourceUsages(out io.Writer, metrics *ResourceMetricsInfo) { - for _, res := range MeasuredResources { +func (printer *TopCmdPrinter) printAllResourceUsages(out io.Writer, metrics *ResourceMetricsInfo, measuredResources []v1.ResourceName) { + for _, res := range measuredResources { + if missingResources, found := printer.nodesMissingResources[metrics.Name]; found && slices.Contains(missingResources, string(res)) { + printSingleMissingResource(out) + continue + } + quantity := metrics.Metrics[res] printSingleResourceUsage(out, res, quantity) fmt.Fprint(out, "\t") if available, found := metrics.Available[res]; found { - fraction := float64(quantity.MilliValue()) / float64(available.MilliValue()) * 100 + fraction := 0.0 + if !available.IsZero() { + fraction = float64(quantity.MilliValue()) / float64(available.MilliValue()) * 100 + } fmt.Fprintf(out, "%d%%\t", int64(fraction)) } } @@ -210,14 +245,19 @@ func printSingleResourceUsage(out io.Writer, resourceType v1.ResourceName, quant switch resourceType { case v1.ResourceCPU: fmt.Fprintf(out, "%vm", quantity.MilliValue()) - case v1.ResourceMemory: + case v1.ResourceMemory, ResourceSwap: fmt.Fprintf(out, "%vMi", quantity.Value()/(1024*1024)) default: fmt.Fprintf(out, "%v", quantity.Value()) } } -func printPodResourcesSum(out io.Writer, total v1.ResourceList, columnWidth int) { +func printSingleMissingResource(out io.Writer) { + const unavailableStr = "" + _, _ = fmt.Fprintf(out, "%s\t%s\t", unavailableStr, unavailableStr) +} + +func (printer *TopCmdPrinter) printPodResourcesSum(out io.Writer, total v1.ResourceList, columnWidth int, measuredResources []v1.ResourceName) { for i := 0; i < columnWidth-2; i++ { printValue(out, "") } @@ -227,10 +267,10 @@ func printPodResourcesSum(out io.Writer, total v1.ResourceList, columnWidth int) for i := 0; i < columnWidth-3; i++ { printValue(out, "") } - printMetricsLine(out, &ResourceMetricsInfo{ + printer.printMetricsLine(out, &ResourceMetricsInfo{ Name: "", Metrics: total, Available: v1.ResourceList{}, - }) + }, measuredResources) } 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 index a2983f83e5f..3a4be863355 100644 --- a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer_test.go +++ b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer_test.go @@ -130,7 +130,7 @@ node1 1000m 10% 1024Mi 10% for _, n := range test.nodes { availableResources[n.Name] = n.Status.Capacity } - top := NewTopCmdPrinter(out) + top := NewTopCmdPrinter(out, false) err := top.PrintNodeMetrics(test.nodeMetric, availableResources, test.noHeader, test.sortBy) assert.Equal(t, test.expectedErr, err) assert.Equal(t, test.expectedOutput, out.String()) @@ -374,7 +374,7 @@ test-1 400m 5120Mi // Create a new TopCmdPrinter with a test writer. _, _, out, _ := genericiooptions.NewTestIOStreams() - top := NewTopCmdPrinter(out) + top := NewTopCmdPrinter(out, false) err := top.PrintPodMetrics(test.podMetric, test.printContainers, test.withNamespace, test.noHeader, test.sortBy, test.sum) assert.Equal(t, test.expectedErr, err) diff --git a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_sorter.go b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_sorter.go index 608d35e4372..3ffb12d98a1 100644 --- a/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_sorter.go +++ b/staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_sorter.go @@ -82,11 +82,11 @@ func (p *PodMetricsSorter) Less(i, j int) bool { } } -func NewPodMetricsSorter(metrics []metricsapi.PodMetrics, withNamespace bool, sortBy string) *PodMetricsSorter { +func NewPodMetricsSorter(metrics []metricsapi.PodMetrics, withNamespace bool, sortBy string, measuredResources []v1.ResourceName) *PodMetricsSorter { var podMetrics = make([]v1.ResourceList, len(metrics)) if len(sortBy) > 0 { for i, v := range metrics { - podMetrics[i] = getPodMetrics(&v) + podMetrics[i] = getPodMetrics(&v, measuredResources) } }