Sort kubectl top output when --sort-by and --containers are used together
- Changed kubectl top to sort first at the pod level and then at the container level when --sort-by and --containers are used together. - Refactored printSinglePodMetrics into two separate functions instead of passing in bool to change behavior. - Refactored MetricsSorters to simplify code. - Added unit tests to test container sorting. - Fixed pod sorting unit tests which were not working because it was checking for --sort-by command line flag which was never true. Kubernetes-commit: 04266b37ded103ddb84a192ec816499904fce1d1
This commit is contained in:
		
							parent
							
								
									00e703ea95
								
							
						
					
					
						commit
						f9c04a0266
					
				| 
						 | 
				
			
			@ -34,7 +34,6 @@ import (
 | 
			
		|||
	"k8s.io/client-go/rest/fake"
 | 
			
		||||
	core "k8s.io/client-go/testing"
 | 
			
		||||
	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
 | 
			
		||||
	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 | 
			
		||||
	"k8s.io/kubectl/pkg/scheme"
 | 
			
		||||
	metricsv1alpha1api "k8s.io/metrics/pkg/apis/metrics/v1alpha1"
 | 
			
		||||
	metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
 | 
			
		||||
| 
						 | 
				
			
			@ -80,15 +79,16 @@ const (
 | 
			
		|||
func TestTopPod(t *testing.T) {
 | 
			
		||||
	testNS := "testns"
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name            string
 | 
			
		||||
		namespace       string
 | 
			
		||||
		options         *TopPodOptions
 | 
			
		||||
		args            []string
 | 
			
		||||
		expectedQuery   string
 | 
			
		||||
		expectedPods    []string
 | 
			
		||||
		namespaces      []string
 | 
			
		||||
		containers      bool
 | 
			
		||||
		listsNamespaces bool
 | 
			
		||||
		name               string
 | 
			
		||||
		namespace          string
 | 
			
		||||
		options            *TopPodOptions
 | 
			
		||||
		args               []string
 | 
			
		||||
		expectedQuery      string
 | 
			
		||||
		expectedPods       []string
 | 
			
		||||
		expectedContainers []string
 | 
			
		||||
		namespaces         []string
 | 
			
		||||
		containers         bool
 | 
			
		||||
		listsNamespaces    bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:            "all namespaces",
 | 
			
		||||
| 
						 | 
				
			
			@ -112,24 +112,56 @@ func TestTopPod(t *testing.T) {
 | 
			
		|||
			namespaces:    []string{testNS, testNS},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "pod with container metrics",
 | 
			
		||||
			options:    &TopPodOptions{PrintContainers: true},
 | 
			
		||||
			args:       []string{"pod1"},
 | 
			
		||||
			name:    "pod with container metrics",
 | 
			
		||||
			options: &TopPodOptions{PrintContainers: true},
 | 
			
		||||
			args:    []string{"pod1"},
 | 
			
		||||
			expectedContainers: []string{
 | 
			
		||||
				"container1-1",
 | 
			
		||||
				"container1-2",
 | 
			
		||||
			},
 | 
			
		||||
			namespaces: []string{testNS},
 | 
			
		||||
			containers: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:         "pod with label sort by cpu",
 | 
			
		||||
			name:         "pod sort by cpu",
 | 
			
		||||
			options:      &TopPodOptions{SortBy: "cpu"},
 | 
			
		||||
			expectedPods: []string{"pod2", "pod3", "pod1"},
 | 
			
		||||
			namespaces:   []string{testNS, testNS, testNS},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:         "pod with label sort by memory",
 | 
			
		||||
			name:         "pod sort by memory",
 | 
			
		||||
			options:      &TopPodOptions{SortBy: "memory"},
 | 
			
		||||
			expectedPods: []string{"pod2", "pod3", "pod1"},
 | 
			
		||||
			namespaces:   []string{testNS, testNS, testNS},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "container sort by cpu",
 | 
			
		||||
			options: &TopPodOptions{PrintContainers: true, SortBy: "cpu"},
 | 
			
		||||
			expectedContainers: []string{
 | 
			
		||||
				"container2-3",
 | 
			
		||||
				"container2-2",
 | 
			
		||||
				"container2-1",
 | 
			
		||||
				"container3-1",
 | 
			
		||||
				"container1-2",
 | 
			
		||||
				"container1-1",
 | 
			
		||||
			},
 | 
			
		||||
			namespaces: []string{testNS, testNS, testNS},
 | 
			
		||||
			containers: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:    "container sort by memory",
 | 
			
		||||
			options: &TopPodOptions{PrintContainers: true, SortBy: "memory"},
 | 
			
		||||
			expectedContainers: []string{
 | 
			
		||||
				"container2-3",
 | 
			
		||||
				"container2-2",
 | 
			
		||||
				"container2-1",
 | 
			
		||||
				"container3-1",
 | 
			
		||||
				"container1-2",
 | 
			
		||||
				"container1-1",
 | 
			
		||||
			},
 | 
			
		||||
			namespaces: []string{testNS, testNS, testNS},
 | 
			
		||||
			containers: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	cmdtesting.InitTestErrorHandler(t)
 | 
			
		||||
	for _, testCase := range testCases {
 | 
			
		||||
| 
						 | 
				
			
			@ -234,23 +266,34 @@ func TestTopPod(t *testing.T) {
 | 
			
		|||
					t.Errorf("unexpected metrics for %s: \n%s", name, result)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if cmdutil.GetFlagString(cmd, "sort-by") == "cpu" || cmdutil.GetFlagString(cmd, "sort-by") == "memory" {
 | 
			
		||||
				resultLines := strings.Split(result, "\n")
 | 
			
		||||
				resultPods := make([]string, len(resultLines)-2) // don't process first (header) and last (empty) line
 | 
			
		||||
 | 
			
		||||
				for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line
 | 
			
		||||
					lineFirstColumn := strings.Split(line, " ")[0]
 | 
			
		||||
					resultPods[i] = lineFirstColumn
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			if testCase.expectedPods != nil {
 | 
			
		||||
				resultPods := getResultColumnValues(result, 0)
 | 
			
		||||
				if !reflect.DeepEqual(testCase.expectedPods, resultPods) {
 | 
			
		||||
					t.Errorf("kinds not matching:\n\texpectedKinds: %v\n\tgotKinds: %v\n", testCase.expectedPods, resultPods)
 | 
			
		||||
					t.Errorf("pods not matching:\n\texpectedPods: %v\n\tresultPods: %v\n", testCase.expectedPods, resultPods)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if testCase.expectedContainers != nil {
 | 
			
		||||
				resultContainers := getResultColumnValues(result, 1)
 | 
			
		||||
				if !reflect.DeepEqual(testCase.expectedContainers, resultContainers) {
 | 
			
		||||
					t.Errorf("containers not matching:\n\texpectedContainers: %v\n\tresultContainers: %v\n", testCase.expectedContainers, resultContainers)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
	for i, line := range resultLines[1 : len(resultLines)-1] { // don't process first (header) and last (empty) line
 | 
			
		||||
		value := strings.Fields(line)[columnIndex]
 | 
			
		||||
		values[i] = value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return values
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTopPodNoResourcesFound(t *testing.T) {
 | 
			
		||||
	testNS := "testns"
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,6 @@ func NewTopCmdPrinter(out io.Writer) *TopCmdPrinter {
 | 
			
		|||
type NodeMetricsSorter struct {
 | 
			
		||||
	metrics []metricsapi.NodeMetrics
 | 
			
		||||
	sortBy  string
 | 
			
		||||
	usages  []v1.ResourceList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *NodeMetricsSorter) Len() int {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,37 +63,24 @@ func (n *NodeMetricsSorter) Len() int {
 | 
			
		|||
 | 
			
		||||
func (n *NodeMetricsSorter) Swap(i, j int) {
 | 
			
		||||
	n.metrics[i], n.metrics[j] = n.metrics[j], n.metrics[i]
 | 
			
		||||
	n.usages[i], n.usages[j] = n.usages[j], n.usages[i]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *NodeMetricsSorter) Less(i, j int) bool {
 | 
			
		||||
	switch n.sortBy {
 | 
			
		||||
	case "cpu":
 | 
			
		||||
		qi := n.usages[i][v1.ResourceCPU]
 | 
			
		||||
		qj := n.usages[j][v1.ResourceCPU]
 | 
			
		||||
		return qi.MilliValue() > qj.MilliValue()
 | 
			
		||||
		return n.metrics[i].Usage.Cpu().MilliValue() > n.metrics[j].Usage.Cpu().MilliValue()
 | 
			
		||||
	case "memory":
 | 
			
		||||
		qi := n.usages[i][v1.ResourceMemory]
 | 
			
		||||
		qj := n.usages[j][v1.ResourceMemory]
 | 
			
		||||
		return qi.Value() > qj.Value()
 | 
			
		||||
		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, error) {
 | 
			
		||||
	var usages = make([]v1.ResourceList, len(metrics))
 | 
			
		||||
	if len(sortBy) > 0 {
 | 
			
		||||
		for i, v := range metrics {
 | 
			
		||||
			v.Usage.DeepCopyInto(&usages[i])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func NewNodeMetricsSorter(metrics []metricsapi.NodeMetrics, sortBy string) *NodeMetricsSorter {
 | 
			
		||||
	return &NodeMetricsSorter{
 | 
			
		||||
		metrics: metrics,
 | 
			
		||||
		sortBy:  sortBy,
 | 
			
		||||
		usages:  usages,
 | 
			
		||||
	}, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PodMetricsSorter struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -116,13 +102,9 @@ func (p *PodMetricsSorter) Swap(i, j int) {
 | 
			
		|||
func (p *PodMetricsSorter) Less(i, j int) bool {
 | 
			
		||||
	switch p.sortBy {
 | 
			
		||||
	case "cpu":
 | 
			
		||||
		qi := p.podMetrics[i][v1.ResourceCPU]
 | 
			
		||||
		qj := p.podMetrics[j][v1.ResourceCPU]
 | 
			
		||||
		return qi.MilliValue() > qj.MilliValue()
 | 
			
		||||
		return p.podMetrics[i].Cpu().MilliValue() > p.podMetrics[j].Cpu().MilliValue()
 | 
			
		||||
	case "memory":
 | 
			
		||||
		qi := p.podMetrics[i][v1.ResourceMemory]
 | 
			
		||||
		qj := p.podMetrics[j][v1.ResourceMemory]
 | 
			
		||||
		return qi.Value() > qj.Value()
 | 
			
		||||
		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
 | 
			
		||||
| 
						 | 
				
			
			@ -131,11 +113,11 @@ func (p *PodMetricsSorter) Less(i, j int) bool {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPodMetricsSorter(metrics []metricsapi.PodMetrics, printContainers bool, withNamespace bool, sortBy string) (*PodMetricsSorter, error) {
 | 
			
		||||
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, printContainers)
 | 
			
		||||
			podMetrics[i] = getPodMetrics(&v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +126,38 @@ func NewPodMetricsSorter(metrics []metricsapi.PodMetrics, printContainers bool,
 | 
			
		|||
		sortBy:        sortBy,
 | 
			
		||||
		withNamespace: withNamespace,
 | 
			
		||||
		podMetrics:    podMetrics,
 | 
			
		||||
	}, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -154,11 +167,7 @@ func (printer *TopCmdPrinter) PrintNodeMetrics(metrics []metricsapi.NodeMetrics,
 | 
			
		|||
	w := printers.GetNewTabWriter(printer.out)
 | 
			
		||||
	defer w.Flush()
 | 
			
		||||
 | 
			
		||||
	n, err := NewNodeMetricsSorter(metrics, sortBy)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	sort.Sort(n)
 | 
			
		||||
	sort.Sort(NewNodeMetricsSorter(metrics, sortBy))
 | 
			
		||||
 | 
			
		||||
	if !noHeaders {
 | 
			
		||||
		printColumnNames(w, NodeColumns)
 | 
			
		||||
| 
						 | 
				
			
			@ -197,16 +206,14 @@ func (printer *TopCmdPrinter) PrintPodMetrics(metrics []metricsapi.PodMetrics, p
 | 
			
		|||
		printColumnNames(w, PodColumns)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	p, err := NewPodMetricsSorter(metrics, printContainers, withNamespace, sortBy)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	sort.Sort(p)
 | 
			
		||||
	sort.Sort(NewPodMetricsSorter(metrics, withNamespace, sortBy))
 | 
			
		||||
 | 
			
		||||
	for _, m := range metrics {
 | 
			
		||||
		err := printSinglePodMetrics(w, &m, printContainers, withNamespace)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		if printContainers {
 | 
			
		||||
			sort.Sort(NewContainerMetricsSorter(m.Containers, sortBy))
 | 
			
		||||
			printSinglePodContainerMetrics(w, &m, withNamespace)
 | 
			
		||||
		} else {
 | 
			
		||||
			printSinglePodMetrics(w, &m, withNamespace)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
| 
						 | 
				
			
			@ -219,56 +226,46 @@ func printColumnNames(out io.Writer, names []string) {
 | 
			
		|||
	fmt.Fprint(out, "\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printSinglePodMetrics(out io.Writer, m *metricsapi.PodMetrics, printContainersOnly bool, withNamespace bool) error {
 | 
			
		||||
	podMetrics, containers, err := getPodMetrics(m, printContainersOnly)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
func printSinglePodMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool) {
 | 
			
		||||
	podMetrics := getPodMetrics(m)
 | 
			
		||||
	if withNamespace {
 | 
			
		||||
		printValue(out, m.Namespace)
 | 
			
		||||
	}
 | 
			
		||||
	if printContainersOnly {
 | 
			
		||||
		for contName := range containers {
 | 
			
		||||
			if withNamespace {
 | 
			
		||||
				printValue(out, m.Namespace)
 | 
			
		||||
			}
 | 
			
		||||
			printValue(out, m.Name)
 | 
			
		||||
			printMetricsLine(out, &ResourceMetricsInfo{
 | 
			
		||||
				Name:      contName,
 | 
			
		||||
				Metrics:   containers[contName],
 | 
			
		||||
				Available: v1.ResourceList{},
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
	printMetricsLine(out, &ResourceMetricsInfo{
 | 
			
		||||
		Name:      m.Name,
 | 
			
		||||
		Metrics:   podMetrics,
 | 
			
		||||
		Available: v1.ResourceList{},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printSinglePodContainerMetrics(out io.Writer, m *metricsapi.PodMetrics, withNamespace bool) {
 | 
			
		||||
	for _, c := range m.Containers {
 | 
			
		||||
		if withNamespace {
 | 
			
		||||
			printValue(out, m.Namespace)
 | 
			
		||||
		}
 | 
			
		||||
		printValue(out, m.Name)
 | 
			
		||||
		printMetricsLine(out, &ResourceMetricsInfo{
 | 
			
		||||
			Name:      m.Name,
 | 
			
		||||
			Metrics:   podMetrics,
 | 
			
		||||
			Name:      c.Name,
 | 
			
		||||
			Metrics:   c.Usage,
 | 
			
		||||
			Available: v1.ResourceList{},
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPodMetrics(m *metricsapi.PodMetrics, printContainersOnly bool) (v1.ResourceList, map[string]v1.ResourceList, error) {
 | 
			
		||||
	containers := make(map[string]v1.ResourceList)
 | 
			
		||||
func getPodMetrics(m *metricsapi.PodMetrics) v1.ResourceList {
 | 
			
		||||
	podMetrics := make(v1.ResourceList)
 | 
			
		||||
	for _, res := range MeasuredResources {
 | 
			
		||||
		podMetrics[res], _ = resource.ParseQuantity("0")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var usage v1.ResourceList
 | 
			
		||||
	for _, c := range m.Containers {
 | 
			
		||||
		c.Usage.DeepCopyInto(&usage)
 | 
			
		||||
		containers[c.Name] = usage
 | 
			
		||||
		if !printContainersOnly {
 | 
			
		||||
			for _, res := range MeasuredResources {
 | 
			
		||||
				quantity := podMetrics[res]
 | 
			
		||||
				quantity.Add(usage[res])
 | 
			
		||||
				podMetrics[res] = quantity
 | 
			
		||||
			}
 | 
			
		||||
		for _, res := range MeasuredResources {
 | 
			
		||||
			quantity := podMetrics[res]
 | 
			
		||||
			quantity.Add(c.Usage[res])
 | 
			
		||||
			podMetrics[res] = quantity
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return podMetrics, containers, nil
 | 
			
		||||
	return podMetrics
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printMetricsLine(out io.Writer, metrics *ResourceMetricsInfo) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue