Skip to content

Commit

Permalink
Added --sum flag to kubectl top pod
Browse files Browse the repository at this point in the history
  • Loading branch information
lauchokyip committed Nov 29, 2021
1 parent 9a75e7b commit 71acf56
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 112 deletions.
8 changes: 5 additions & 3 deletions staging/src/k8s.io/kubectl/pkg/cmd/top/top_pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"fmt"
"time"

"k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -52,6 +52,7 @@ type TopPodOptions struct {
PrintContainers bool
NoHeaders bool
UseProtocolBuffers bool
Sum bool

PodClient corev1client.PodsGetter
Printer *metricsutil.TopCmdPrinter
Expand Down Expand Up @@ -115,6 +116,7 @@ func NewCmdTopPod(f cmdutil.Factory, o *TopPodOptions, streams genericclioptions
cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
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")
return cmd
}

Expand Down Expand Up @@ -215,7 +217,7 @@ func (o TopPodOptions) RunTopPod() error {
}
}

return o.Printer.PrintPodMetrics(metrics.Items, o.PrintContainers, o.AllNamespaces, o.NoHeaders, o.SortBy)
return o.Printer.PrintPodMetrics(metrics.Items, o.PrintContainers, o.AllNamespaces, o.NoHeaders, o.SortBy, o.Sum)
}

func getMetricsFromMetricsAPI(metricsClient metricsclientset.Interface, namespace, resourceName string, allNamespaces bool, labelSelector labels.Selector, fieldSelector fields.Selector) (*metricsapi.PodMetricsList, error) {
Expand Down Expand Up @@ -274,7 +276,7 @@ func verifyEmptyMetrics(o TopPodOptions, labelSelector labels.Selector, fieldSel
return errors.New("metrics not available yet")
}

func checkPodAge(pod *v1.Pod) error {
func checkPodAge(pod *corev1.Pod) error {
age := time.Since(pod.CreationTimestamp.Time)
if age > metricsCreationDelay {
message := fmt.Sprintf("Metrics not available for pod %s/%s, age: %s", pod.Namespace, pod.Name, age.String())
Expand Down
142 changes: 33 additions & 109 deletions staging/src/k8s.io/kubectl/pkg/metricsutil/metrics_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,114 +52,6 @@ func NewTopCmdPrinter(out io.Writer) *TopCmdPrinter {
return &TopCmdPrinter{out: out}
}

type NodeMetricsSorter struct {
metrics []metricsapi.NodeMetrics
sortBy string
}

func (n *NodeMetricsSorter) Len() int {
return len(n.metrics)
}

func (n *NodeMetricsSorter) Swap(i, j int) {
n.metrics[i], n.metrics[j] = n.metrics[j], n.metrics[i]
}

func (n *NodeMetricsSorter) Less(i, j int) bool {
switch n.sortBy {
case "cpu":
return n.metrics[i].Usage.Cpu().MilliValue() > n.metrics[j].Usage.Cpu().MilliValue()
case "memory":
return n.metrics[i].Usage.Memory().Value() > n.metrics[j].Usage.Memory().Value()
default:
return n.metrics[i].Name < n.metrics[j].Name
}
}

func NewNodeMetricsSorter(metrics []metricsapi.NodeMetrics, sortBy string) *NodeMetricsSorter {
return &NodeMetricsSorter{
metrics: metrics,
sortBy: sortBy,
}
}

type PodMetricsSorter struct {
metrics []metricsapi.PodMetrics
sortBy string
withNamespace bool
podMetrics []v1.ResourceList
}

func (p *PodMetricsSorter) Len() int {
return len(p.metrics)
}

func (p *PodMetricsSorter) Swap(i, j int) {
p.metrics[i], p.metrics[j] = p.metrics[j], p.metrics[i]
p.podMetrics[i], p.podMetrics[j] = p.podMetrics[j], p.podMetrics[i]
}

func (p *PodMetricsSorter) Less(i, j int) bool {
switch p.sortBy {
case "cpu":
return p.podMetrics[i].Cpu().MilliValue() > p.podMetrics[j].Cpu().MilliValue()
case "memory":
return p.podMetrics[i].Memory().Value() > p.podMetrics[j].Memory().Value()
default:
if p.withNamespace && p.metrics[i].Namespace != p.metrics[j].Namespace {
return p.metrics[i].Namespace < p.metrics[j].Namespace
}
return p.metrics[i].Name < p.metrics[j].Name
}
}

func NewPodMetricsSorter(metrics []metricsapi.PodMetrics, withNamespace bool, sortBy string) *PodMetricsSorter {
var podMetrics = make([]v1.ResourceList, len(metrics))
if len(sortBy) > 0 {
for i, v := range metrics {
podMetrics[i] = getPodMetrics(&v)
}
}

return &PodMetricsSorter{
metrics: metrics,
sortBy: sortBy,
withNamespace: withNamespace,
podMetrics: podMetrics,
}
}

type ContainerMetricsSorter struct {
metrics []metricsapi.ContainerMetrics
sortBy string
}

func (s *ContainerMetricsSorter) Len() int {
return len(s.metrics)
}

func (s *ContainerMetricsSorter) Swap(i, j int) {
s.metrics[i], s.metrics[j] = s.metrics[j], s.metrics[i]
}

func (s *ContainerMetricsSorter) Less(i, j int) bool {
switch s.sortBy {
case "cpu":
return s.metrics[i].Usage.Cpu().MilliValue() > s.metrics[j].Usage.Cpu().MilliValue()
case "memory":
return s.metrics[i].Usage.Memory().Value() > s.metrics[j].Usage.Memory().Value()
default:
return s.metrics[i].Name < s.metrics[j].Name
}
}

func NewContainerMetricsSorter(metrics []metricsapi.ContainerMetrics, sortBy string) *ContainerMetricsSorter {
return &ContainerMetricsSorter{
metrics: metrics,
sortBy: sortBy,
}
}

func (printer *TopCmdPrinter) PrintNodeMetrics(metrics []metricsapi.NodeMetrics, availableResources map[string]v1.ResourceList, noHeaders bool, sortBy string) error {
if len(metrics) == 0 {
return nil
Expand Down Expand Up @@ -190,18 +82,22 @@ func (printer *TopCmdPrinter) PrintNodeMetrics(metrics []metricsapi.NodeMetrics,
return nil
}

func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, printContainers bool, withNamespace bool, noHeaders bool, sortBy string) error {
func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, printContainers bool, withNamespace bool, noHeaders bool, sortBy string, sum bool) error {
if len(metrics) == 0 {
return nil
}
w := printers.GetNewTabWriter(printer.out)
defer w.Flush()

columnWidth := len(PodColumns)
if !noHeaders {
if withNamespace {
printValue(w, NamespaceColumn)
columnWidth++
}
if printContainers {
printValue(w, PodColumn)
columnWidth++
}
printColumnNames(w, PodColumns)
}
Expand All @@ -215,7 +111,17 @@ func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, p
} else {
printSinglePodMetrics(w, &m, withNamespace)
}

}

if sum {
adder := NewResourceAdder(MeasuredResources)
for _, m := range metrics {
adder.AddPodMetrics(&m)
}
printPodResourcesSum(w, adder.total, columnWidth)
}

return nil
}

Expand Down Expand Up @@ -310,3 +216,21 @@ func printSingleResourceUsage(out io.Writer, resourceType v1.ResourceName, quant
fmt.Fprintf(out, "%v", quantity.Value())
}
}

func printPodResourcesSum(out io.Writer, total v1.ResourceList, columnWidth int) {
for i := 0; i < columnWidth-2; i++ {
printValue(out, "")
}
printValue(out, "________")
printValue(out, "________")
fmt.Fprintf(out, "\n")
for i := 0; i < columnWidth-3; i++ {
printValue(out, "")
}
printMetricsLine(out, &ResourceMetricsInfo{
Name: "",
Metrics: total,
Available: v1.ResourceList{},
})

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
Copyright 2021 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 (
corev1 "k8s.io/api/core/v1"
metricsapi "k8s.io/metrics/pkg/apis/metrics"
)

type ResourceAdder struct {
resources []corev1.ResourceName
total corev1.ResourceList
}

func NewResourceAdder(resources []corev1.ResourceName) *ResourceAdder {
return &ResourceAdder{
resources: resources,
total: make(corev1.ResourceList),
}
}

// AddPodMetrics adds each pod metric to the total
func (adder *ResourceAdder) AddPodMetrics(m *metricsapi.PodMetrics) {
for _, c := range m.Containers {
for _, res := range adder.resources {
total := adder.total[res]
total.Add(c.Usage[res])
adder.total[res] = total
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
Copyright 2021 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"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/metrics/pkg/apis/metrics"
)

func getResourceQuantity(t *testing.T, quantityStr string) resource.Quantity {
t.Helper()
var err error
quantity, err := resource.ParseQuantity("0")
if err != nil {
t.Errorf("failed when parsing 0 into resource.Quantity")
}
if quantityStr != "" {
quantity, err = resource.ParseQuantity(quantityStr)
if err != nil {
t.Errorf("%s is not a valid resource value", quantityStr)
}
}
return quantity
}

func addContainerMetricsToPodMetrics(t *testing.T, podMetrics *metrics.PodMetrics, cpuUsage, memUsage string) {
t.Helper()
containerMetrics := metrics.ContainerMetrics{
Usage: corev1.ResourceList{},
}

containerMetrics.Usage["cpu"] = getResourceQuantity(t, cpuUsage)
containerMetrics.Usage["memory"] = getResourceQuantity(t, memUsage)

podMetrics.Containers = append(podMetrics.Containers, containerMetrics)
}

func initResourceAdder() *ResourceAdder {
resources := []corev1.ResourceName{
corev1.ResourceCPU,
corev1.ResourceMemory,
}
return NewResourceAdder(resources)
}

func TestAddPodMetrics(t *testing.T) {
resourceAdder := initResourceAdder()

tests := []struct {
name string
cpuUsage string
memUsage string
expectedCpuUsage resource.Quantity
expectedMemUsage resource.Quantity
}{
{
name: "initial value",
cpuUsage: "0",
memUsage: "0",
expectedCpuUsage: getResourceQuantity(t, "0"),
expectedMemUsage: getResourceQuantity(t, "0"),
},
{
name: "add first container metric",
cpuUsage: "1m",
memUsage: "10Mi",
expectedCpuUsage: getResourceQuantity(t, "1m"),
expectedMemUsage: getResourceQuantity(t, "10Mi"),
},
{
name: "add second container metric",
cpuUsage: "5m",
memUsage: "25Mi",
expectedCpuUsage: getResourceQuantity(t, "6m"),
expectedMemUsage: getResourceQuantity(t, "35Mi"),
},
{
name: "add third container zero metric",
cpuUsage: "0m",
memUsage: "0Mi",
expectedCpuUsage: getResourceQuantity(t, "6m"),
expectedMemUsage: getResourceQuantity(t, "35Mi"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
podMetrics := metrics.PodMetrics{}
addContainerMetricsToPodMetrics(t, &podMetrics, test.cpuUsage, test.memUsage)

resourceAdder.AddPodMetrics(&podMetrics)
cpuUsage := resourceAdder.total["cpu"]
memUsage := resourceAdder.total["memory"]

if !test.expectedCpuUsage.Equal(cpuUsage) {
t.Errorf("expecting cpu usage %s but getting %s", test.expectedCpuUsage.String(), cpuUsage.String())
}
if !test.expectedMemUsage.Equal(memUsage) {
t.Errorf("expecting memeory usage %s but getting %s", test.expectedMemUsage.String(), memUsage.String())
}
})
}
}

0 comments on commit 71acf56

Please sign in to comment.