[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 <iholder@redhat.com>

* 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 <iholder@redhat.com>

* 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 <iholder@redhat.com>

* kubectl top node --show-swap: add unit tests

Signed-off-by: Itamar Holder <iholder@redhat.com>

* kubectl top pod --show-swap: Add unit tests

Signed-off-by: Itamar Holder <iholder@redhat.com>

* Explicitly mark swap as unavailable when necessary

Signed-off-by: Itamar Holder <iholder@redhat.com>

---------

Signed-off-by: Itamar Holder <iholder@redhat.com>
This commit is contained in:
Itamar Holder
2025-06-04 17:10:38 +03:00
committed by GitHub
parent caa9324842
commit 8d3fb9ee0a
8 changed files with 327 additions and 48 deletions

View File

@@ -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)

View File

@@ -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, "<unknown>") {
t.Errorf("expected swap to be <unknown>: \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)
}
}
})
}
}

View File

@@ -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
}

View File

@@ -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),
},
},

View File

@@ -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))),
},
},
},
},
},

View File

@@ -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 := "<unknown>"
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 = "<unknown>"
_, _ = 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)
}

View File

@@ -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)

View File

@@ -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)
}
}