Add field based sorting to the kubectl command line.

This commit is contained in:
Brendan Burns
2015-08-07 23:04:25 -07:00
parent 68f90fd526
commit 99b8df1812
21 changed files with 384 additions and 18 deletions

View File

@@ -471,6 +471,7 @@ func (f *Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMappin
if err != nil {
return nil, err
}
printer = maybeWrapSortingPrinter(cmd, printer)
}
return printer, nil
}

View File

@@ -32,6 +32,7 @@ func AddPrinterFlags(cmd *cobra.Command) {
cmd.Flags().String("output-version", "", "Output the formatted object with the given version (default api-version).")
cmd.Flags().Bool("no-headers", false, "When using the default output, don't print headers.")
cmd.Flags().StringP("template", "t", "", "Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]")
cmd.Flags().String("sort-by", "", "If non-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. 'ObjectMeta.Name'). The field in the API resource specified by this JSONPath expression must be an integer or a string.")
}
// AddOutputFlagsForMutation adds output related flags to a command. Used by mutations only.
@@ -86,5 +87,21 @@ func PrinterForCommand(cmd *cobra.Command) (kubectl.ResourcePrinter, bool, error
outputFormat = "template"
}
return kubectl.GetPrinter(outputFormat, templateFile)
printer, generic, err := kubectl.GetPrinter(outputFormat, templateFile)
if err != nil {
return nil, generic, err
}
return maybeWrapSortingPrinter(cmd, printer), generic, nil
}
func maybeWrapSortingPrinter(cmd *cobra.Command, printer kubectl.ResourcePrinter) kubectl.ResourcePrinter {
sorting := GetFlagString(cmd, "sort-by")
if len(sorting) != 0 {
return &kubectl.SortingPrinter{
Delegate: printer,
SortField: fmt.Sprintf("{%s}", sorting),
}
}
return printer
}

View File

@@ -0,0 +1,123 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 kubectl
import (
"fmt"
"io"
"reflect"
"sort"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/jsonpath"
)
// Sorting printer sorts list types before delegating to another printer.
// Non-list types are simply passed through
type SortingPrinter struct {
SortField string
Delegate ResourcePrinter
}
func (s *SortingPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
if !runtime.IsListType(obj) {
fmt.Fprintf(out, "Not a list, skipping: %#v\n", obj)
return s.Delegate.PrintObj(obj, out)
}
if err := s.sortObj(obj); err != nil {
return err
}
return s.Delegate.PrintObj(obj, out)
}
func (s *SortingPrinter) sortObj(obj runtime.Object) error {
objs, err := runtime.ExtractList(obj)
if err != nil {
return err
}
if len(objs) == 0 {
return nil
}
parser := jsonpath.New("sorting")
parser.Parse(s.SortField)
values, err := parser.FindResults(reflect.ValueOf(objs[0]).Elem().Interface())
if err != nil {
return err
}
if len(values) == 0 {
return fmt.Errorf("couldn't find any field with path: %s", s.SortField)
}
sorter := &RuntimeSort{
field: s.SortField,
objs: objs,
}
sort.Sort(sorter)
runtime.SetList(obj, sorter.objs)
return nil
}
// RuntimeSort is an implementation of the golang sort interface that knows how to sort
// lists of runtime.Object
type RuntimeSort struct {
field string
objs []runtime.Object
}
func (r *RuntimeSort) Len() int {
return len(r.objs)
}
func (r *RuntimeSort) Swap(i, j int) {
r.objs[i], r.objs[j] = r.objs[j], r.objs[i]
}
func (r *RuntimeSort) Less(i, j int) bool {
iObj := r.objs[i]
jObj := r.objs[j]
parser := jsonpath.New("sorting")
parser.Parse(r.field)
iValues, err := parser.FindResults(reflect.ValueOf(iObj).Elem().Interface())
if err != nil {
glog.Fatalf("Failed to get i values for %#v using %s (%#v)", iObj, r.field, err)
}
jValues, err := parser.FindResults(reflect.ValueOf(jObj).Elem().Interface())
if err != nil {
glog.Fatalf("Failed to get j values for %#v using %s (%v)", jObj, r.field, err)
}
iField := iValues[0][0]
jField := jValues[0][0]
switch iField.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return iField.Int() < jField.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return iField.Uint() < jField.Uint()
case reflect.Float32, reflect.Float64:
return iField.Float() < jField.Float()
case reflect.String:
return iField.String() < jField.String()
default:
glog.Fatalf("Field %s in %v is an unsortable type: %s", r.field, iObj, iField.Kind().String())
}
// default to preserving order
return i < j
}

View File

@@ -0,0 +1,171 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 kubectl
import (
"reflect"
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/runtime"
)
func TestSortingPrinter(t *testing.T) {
tests := []struct {
obj runtime.Object
sort runtime.Object
field string
name string
}{
{
name: "in-order-already",
obj: &api.PodList{
Items: []api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "a",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "b",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "c",
},
},
},
},
sort: &api.PodList{
Items: []api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "a",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "b",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "c",
},
},
},
},
field: "{.ObjectMeta.Name}",
},
{
name: "reverse-order",
obj: &api.PodList{
Items: []api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "b",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "c",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "a",
},
},
},
},
sort: &api.PodList{
Items: []api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "a",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "b",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "c",
},
},
},
},
field: "{.ObjectMeta.Name}",
},
{
name: "random-order-numbers",
obj: &api.ReplicationControllerList{
Items: []api.ReplicationController{
{
Spec: api.ReplicationControllerSpec{
Replicas: 5,
},
},
{
Spec: api.ReplicationControllerSpec{
Replicas: 1,
},
},
{
Spec: api.ReplicationControllerSpec{
Replicas: 9,
},
},
},
},
sort: &api.ReplicationControllerList{
Items: []api.ReplicationController{
{
Spec: api.ReplicationControllerSpec{
Replicas: 1,
},
},
{
Spec: api.ReplicationControllerSpec{
Replicas: 5,
},
},
{
Spec: api.ReplicationControllerSpec{
Replicas: 9,
},
},
},
},
field: "{.Spec.Replicas}",
},
}
for _, test := range tests {
sort := &SortingPrinter{SortField: test.field}
if err := sort.sortObj(test.obj); err != nil {
t.Errorf("unexpected error: %v (%s)", err, test.name)
continue
}
if !reflect.DeepEqual(test.obj, test.sort) {
t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v", test.name, test.sort, test.obj)
}
}
}

View File

@@ -53,17 +53,31 @@ func (j *JSONPath) Parse(text string) (err error) {
// Execute bounds data into template and write the result
func (j *JSONPath) Execute(wr io.Writer, data interface{}) error {
fullResults, err := j.FindResults(data)
if err != nil {
return err
}
for ix := range fullResults {
if err := j.PrintResults(wr, fullResults[ix]); err != nil {
return err
}
}
return nil
}
func (j *JSONPath) FindResults(data interface{}) ([][]reflect.Value, error) {
if j.parser == nil {
return fmt.Errorf("%s is an incomplete jsonpath template", j.name)
return nil, fmt.Errorf("%s is an incomplete jsonpath template", j.name)
}
j.cur = []reflect.Value{reflect.ValueOf(data)}
nodes := j.parser.Root.Nodes
fullResult := [][]reflect.Value{}
for i := 0; i < len(nodes); i++ {
node := nodes[i]
results, err := j.walk(j.cur, node)
if err != nil {
return err
return nil, err
}
//encounter an end node, break the current block
@@ -80,20 +94,17 @@ func (j *JSONPath) Execute(wr io.Writer, data interface{}) error {
if k == len(results)-1 {
j.inRange -= 1
}
err := j.Execute(wr, value.Interface())
nextResults, err := j.FindResults(value.Interface())
if err != nil {
return err
return nil, err
}
fullResult = append(fullResult, nextResults...)
}
break
}
err = j.PrintResults(wr, results)
if err != nil {
return err
}
fullResult = append(fullResult, results)
}
return nil
return fullResult, nil
}
// PrintResults write the results into writer