mirror of
https://github.com/outbackdingo/kubernetes.git
synced 2026-01-27 18:19:28 +00:00
[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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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))),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user