Merge pull request #105100 from lauchokyip/addTopSum

Added --sum flag to kubectl top pod

Kubernetes-commit: 0401cc2762b65de5b1bd2e61ed77a1a4a359b80b
This commit is contained in:
Kubernetes Publisher 2022-05-03 17:17:41 -07:00
commit 3dd3651a97
7 changed files with 336 additions and 116 deletions

4
go.mod
View File

@ -33,7 +33,7 @@ require (
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.0.0-20220413163940-7a897301bd56
k8s.io/apimachinery v0.0.0-20220406001655-080c0c77fab5
k8s.io/cli-runtime v0.0.0-20220414205729-b9e8611a3781
k8s.io/cli-runtime v0.0.0-20220504025609-ddf455f66463
k8s.io/client-go v0.0.0-20220413164420-28ccde769fc5
k8s.io/component-base v0.0.0-20220331212037-3b9b201c27aa
k8s.io/component-helpers v0.0.0-20220330052013-855d491da0b6
@ -50,7 +50,7 @@ require (
replace (
k8s.io/api => k8s.io/api v0.0.0-20220413163940-7a897301bd56
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20220406001655-080c0c77fab5
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20220414205729-b9e8611a3781
k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20220504025609-ddf455f66463
k8s.io/client-go => k8s.io/client-go v0.0.0-20220413164420-28ccde769fc5
k8s.io/code-generator => k8s.io/code-generator v0.0.0-20220330050606-8f17de063761
k8s.io/component-base => k8s.io/component-base v0.0.0-20220331212037-3b9b201c27aa

4
go.sum
View File

@ -882,8 +882,8 @@ k8s.io/api v0.0.0-20220413163940-7a897301bd56 h1:kCg7nKtlLw11+FZqIc2ceS/j2guTgA4
k8s.io/api v0.0.0-20220413163940-7a897301bd56/go.mod h1:jimmvAe8QfauNx6CM87E20mTsK3CPgiOzVhfaNASkJc=
k8s.io/apimachinery v0.0.0-20220406001655-080c0c77fab5 h1:nG9Zc74nUlkVHMapMJVjlH8hT3z/fBVixlVfrsQgi24=
k8s.io/apimachinery v0.0.0-20220406001655-080c0c77fab5/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM=
k8s.io/cli-runtime v0.0.0-20220414205729-b9e8611a3781 h1:Zk9QAllFqx36NRY6EHxy7D5hLIoyj0C1WgoeJT/fzvQ=
k8s.io/cli-runtime v0.0.0-20220414205729-b9e8611a3781/go.mod h1:QPB7YO7HzmeJ2395hCEgbQFW3niSRHB9AXZpY2EFzAo=
k8s.io/cli-runtime v0.0.0-20220504025609-ddf455f66463 h1:Ici2x2ahD3k0wJrSae1tKf+EcRyRLqCVE8A5UtOvHjo=
k8s.io/cli-runtime v0.0.0-20220504025609-ddf455f66463/go.mod h1:QPB7YO7HzmeJ2395hCEgbQFW3niSRHB9AXZpY2EFzAo=
k8s.io/client-go v0.0.0-20220413164420-28ccde769fc5 h1:rWJnUu0XjscMYcqmyM49Nsu7w1pJ7PoFbFZZAbCw1AI=
k8s.io/client-go v0.0.0-20220413164420-28ccde769fc5/go.mod h1:2AylUVFyWxiRcOPb3uDGc47Y2p3RhD+aFQBxpkNXB74=
k8s.io/code-generator v0.0.0-20220330050606-8f17de063761/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w=

View File

@ -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"
@ -52,6 +52,7 @@ type TopPodOptions struct {
PrintContainers bool
NoHeaders bool
UseProtocolBuffers bool
Sum bool
PodClient corev1client.PodsGetter
Printer *metricsutil.TopCmdPrinter
@ -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
}
@ -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) {
@ -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())

View File

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

View File

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

View File

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

View File

@ -0,0 +1,130 @@
/*
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 (
"k8s.io/api/core/v1"
metricsapi "k8s.io/metrics/pkg/apis/metrics"
)
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,
}
}