diff --git a/pkg/cmd/top/top_pod.go b/pkg/cmd/top/top_pod.go index 4bc2934a..175936c0 100644 --- a/pkg/cmd/top/top_pod.go +++ b/pkg/cmd/top/top_pod.go @@ -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()) diff --git a/pkg/metricsutil/metrics_printer.go b/pkg/metricsutil/metrics_printer.go index f199dfd6..17091d92 100644 --- a/pkg/metricsutil/metrics_printer.go +++ b/pkg/metricsutil/metrics_printer.go @@ -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{}, + }) + +} diff --git a/pkg/metricsutil/metrics_resource_adder.go b/pkg/metricsutil/metrics_resource_adder.go new file mode 100644 index 00000000..b0ee3f22 --- /dev/null +++ b/pkg/metricsutil/metrics_resource_adder.go @@ -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 + } + } +} diff --git a/pkg/metricsutil/metrics_resource_adder_test.go b/pkg/metricsutil/metrics_resource_adder_test.go new file mode 100644 index 00000000..f5a22a74 --- /dev/null +++ b/pkg/metricsutil/metrics_resource_adder_test.go @@ -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()) + } + }) + } +} diff --git a/pkg/metricsutil/metrics_sorter.go b/pkg/metricsutil/metrics_sorter.go new file mode 100644 index 00000000..608d35e4 --- /dev/null +++ b/pkg/metricsutil/metrics_sorter.go @@ -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, + } +}