From de146237755b58d61fd10142e69235334f1ae5ce Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Thu, 13 Aug 2015 14:11:23 -0700 Subject: [PATCH] Add a printer that knows how to print user-defined columns --- pkg/kubectl/custom_column_printer.go | 107 ++++++++++++++++++++++ pkg/kubectl/custom_column_printer_test.go | 93 +++++++++++++++++++ pkg/kubectl/resource_printer.go | 29 +++--- pkg/kubectl/resource_printer_test.go | 11 ++- pkg/kubectl/sorting_printer.go | 32 ++++--- pkg/kubectl/sorting_printer_test.go | 22 +++-- pkg/util/jsonpath/jsonpath.go | 43 ++++++++- 7 files changed, 302 insertions(+), 35 deletions(-) create mode 100644 pkg/kubectl/custom_column_printer.go create mode 100644 pkg/kubectl/custom_column_printer_test.go diff --git a/pkg/kubectl/custom_column_printer.go b/pkg/kubectl/custom_column_printer.go new file mode 100644 index 00000000000..bbbb16e1444 --- /dev/null +++ b/pkg/kubectl/custom_column_printer.go @@ -0,0 +1,107 @@ +/* +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" + "strings" + "text/tabwriter" + + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/jsonpath" +) + +const ( + columnwidth = 10 + tabwidth = 4 + padding = 3 + padding_character = ' ' + flags = 0 +) + +// Column represents a user specified column +type Column struct { + // The header to print above the column, general style is ALL_CAPS + Header string + // The pointer to the field in the object to print in JSONPath form + // e.g. {.ObjectMeta.Name}, see pkg/util/jsonpath for more details. + FieldSpec string +} + +// CustomColumnPrinter is a printer that knows how to print arbitrary columns +// of data from templates specified in the `Columns` array +type CustomColumnsPrinter struct { + Columns []Column +} + +func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error { + w := tabwriter.NewWriter(out, columnwidth, tabwidth, padding, padding_character, flags) + headers := make([]string, len(s.Columns)) + for ix := range s.Columns { + headers[ix] = s.Columns[ix].Header + } + fmt.Fprintln(w, strings.Join(headers, "\t")) + parsers := make([]*jsonpath.JSONPath, len(s.Columns)) + for ix := range s.Columns { + parsers[ix] = jsonpath.New(fmt.Sprintf("column%d", ix)) + if err := parsers[ix].Parse(s.Columns[ix].FieldSpec); err != nil { + return err + } + } + + if runtime.IsListType(obj) { + objs, err := runtime.ExtractList(obj) + if err != nil { + return err + } + for ix := range objs { + if err := s.printOneObject(objs[ix], parsers, w); err != nil { + return err + } + } + } else { + if err := s.printOneObject(obj, parsers, w); err != nil { + return err + } + } + return w.Flush() +} + +func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jsonpath.JSONPath, out io.Writer) error { + columns := make([]string, len(parsers)) + for ix := range parsers { + parser := parsers[ix] + values, err := parser.FindResults(reflect.ValueOf(obj).Elem().Interface()) + if err != nil { + return err + } + if len(values) == 0 || len(values[0]) == 0 { + fmt.Fprintf(out, "\t") + } + valueStrings := []string{} + for arrIx := range values { + for valIx := range values[arrIx] { + valueStrings = append(valueStrings, fmt.Sprintf("%v", values[arrIx][valIx].Interface())) + } + } + columns[ix] = strings.Join(valueStrings, ",") + } + fmt.Fprintln(out, strings.Join(columns, "\t")) + return nil +} diff --git a/pkg/kubectl/custom_column_printer_test.go b/pkg/kubectl/custom_column_printer_test.go new file mode 100644 index 00000000000..62c3aea9dc6 --- /dev/null +++ b/pkg/kubectl/custom_column_printer_test.go @@ -0,0 +1,93 @@ +/* +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 ( + "bytes" + "testing" + + "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/runtime" +) + +func TestColumnPrint(t *testing.T) { + tests := []struct { + columns []Column + obj runtime.Object + expectedOutput string + }{ + { + columns: []Column{ + { + Header: "NAME", + FieldSpec: "{.metadata.name}", + }, + }, + obj: &v1.Pod{ObjectMeta: v1.ObjectMeta{Name: "foo"}}, + expectedOutput: `NAME +foo +`, + }, + { + columns: []Column{ + { + Header: "NAME", + FieldSpec: "{.metadata.name}", + }, + }, + obj: &v1.PodList{ + Items: []v1.Pod{ + {ObjectMeta: v1.ObjectMeta{Name: "foo"}}, + {ObjectMeta: v1.ObjectMeta{Name: "bar"}}, + }, + }, + expectedOutput: `NAME +foo +bar +`, + }, + { + columns: []Column{ + { + Header: "NAME", + FieldSpec: "{.metadata.name}", + }, + { + Header: "API_VERSION", + FieldSpec: "{.apiVersion}", + }, + }, + obj: &v1.Pod{ObjectMeta: v1.ObjectMeta{Name: "foo"}, TypeMeta: v1.TypeMeta{APIVersion: "baz"}}, + expectedOutput: `NAME API_VERSION +foo baz +`, + }, + } + + for _, test := range tests { + printer := &CustomColumnsPrinter{ + Columns: test.columns, + } + buffer := &bytes.Buffer{} + if err := printer.PrintObj(test.obj, buffer); err != nil { + t.Errorf("unexpected error: %v", err) + } + if buffer.String() != test.expectedOutput { + t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", test.expectedOutput, buffer.String()) + } + } +} diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 3d649a6efc1..d16c5ba87a3 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -34,6 +34,7 @@ import ( "github.com/golang/glog" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/conversion" "k8s.io/kubernetes/pkg/expapi" "k8s.io/kubernetes/pkg/labels" @@ -1335,20 +1336,26 @@ func NewJSONPathPrinter(tmpl string) (*JSONPathPrinter, error) { // PrintObj formats the obj with the JSONPath Template. func (j *JSONPathPrinter) PrintObj(obj runtime.Object, w io.Writer) error { - data, err := json.Marshal(obj) - if err != nil { - return err + var queryObj interface{} + switch obj.(type) { + case *v1.List, *api.List: + data, err := json.Marshal(obj) + if err != nil { + return err + } + queryObj = map[string]interface{}{} + if err := json.Unmarshal(data, &queryObj); err != nil { + return err + } + default: + queryObj = obj } - out := map[string]interface{}{} - if err := json.Unmarshal(data, &out); err != nil { - return err - } - if err = j.JSONPath.Execute(w, out); err != nil { + + if err := j.JSONPath.Execute(w, queryObj); err != nil { fmt.Fprintf(w, "Error executing template: %v\n", err) fmt.Fprintf(w, "template was:\n\t%v\n", j.rawTemplate) - fmt.Fprintf(w, "raw data was:\n\t%v\n", string(data)) - fmt.Fprintf(w, "object given to template engine was:\n\t%+v\n", out) - return fmt.Errorf("error executing jsonpath '%v': '%v'\n----data----\n%+v\n", j.rawTemplate, err, out) + fmt.Fprintf(w, "object given to jsonpath engine was:\n\t%#v\n", queryObj) + return fmt.Errorf("error executing jsonpath '%v': '%v'\n----data----\n%+v\n", j.rawTemplate, err, obj) } return nil } diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go index 476e222ffe6..0a7e654c194 100644 --- a/pkg/kubectl/resource_printer_test.go +++ b/pkg/kubectl/resource_printer_test.go @@ -102,6 +102,13 @@ func TestPrinter(t *testing.T) { //test inputs simpleTest := &TestPrintType{"foo"} podTest := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} + podListTest := &api.PodList{ + Items: []api.Pod{ + {ObjectMeta: api.ObjectMeta{Name: "foo"}}, + {ObjectMeta: api.ObjectMeta{Name: "bar"}}, + }, + } + emptyListTest := &api.PodList{} testapi, err := api.Scheme.ConvertToVersion(podTest, testapi.Version()) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -119,6 +126,8 @@ func TestPrinter(t *testing.T) { {"test template", "template", "{{if .id}}{{.id}}{{end}}{{if .metadata.name}}{{.metadata.name}}{{end}}", podTest, "foo"}, {"test jsonpath", "jsonpath", "{.metadata.name}", podTest, "foo"}, + {"test jsonpath list", "jsonpath", "{.items[*].metadata.name}", podListTest, "foo bar"}, + {"test jsonpath empty list", "jsonpath", "{.items[*].metadata.name}", emptyListTest, ""}, {"test name", "name", "", podTest, "/foo\n"}, {"emits versioned objects", "template", "{{.kind}}", testapi, "Pod"}, } @@ -132,7 +141,7 @@ func TestPrinter(t *testing.T) { t.Errorf("unexpected error: %#v", err) } if buf.String() != test.Expect { - t.Errorf("in %s, expect %q, got %q", test.Name, test.Expect, buf.String(), buf.String()) + t.Errorf("in %s, expect %q, got %q", test.Name, test.Expect, buf.String()) } } diff --git a/pkg/kubectl/sorting_printer.go b/pkg/kubectl/sorting_printer.go index d536914a31c..b28724a4a94 100644 --- a/pkg/kubectl/sorting_printer.go +++ b/pkg/kubectl/sorting_printer.go @@ -87,6 +87,23 @@ func (r *RuntimeSort) Swap(i, j int) { r.objs[i], r.objs[j] = r.objs[j], r.objs[i] } +func isLess(i, j reflect.Value) (bool, error) { + switch i.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return i.Int() < j.Int(), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return i.Uint() < j.Uint(), nil + case reflect.Float32, reflect.Float64: + return i.Float() < j.Float(), nil + case reflect.String: + return i.String() < j.String(), nil + case reflect.Ptr: + return isLess(i.Elem(), j.Elem()) + default: + return false, fmt.Errorf("unsortable type: %v", i.Kind()) + } +} + func (r *RuntimeSort) Less(i, j int) bool { iObj := r.objs[i] jObj := r.objs[j] @@ -106,18 +123,9 @@ func (r *RuntimeSort) Less(i, j int) bool { 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: + less, err := isLess(iField, jField) + if err != nil { glog.Fatalf("Field %s in %v is an unsortable type: %s", r.field, iObj, iField.Kind().String()) } - // default to preserving order - return i < j + return less } diff --git a/pkg/kubectl/sorting_printer_test.go b/pkg/kubectl/sorting_printer_test.go index 7dd978215f6..9abb1e701d6 100644 --- a/pkg/kubectl/sorting_printer_test.go +++ b/pkg/kubectl/sorting_printer_test.go @@ -20,11 +20,13 @@ import ( "reflect" "testing" - "k8s.io/kubernetes/pkg/api" + api "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/runtime" ) func TestSortingPrinter(t *testing.T) { + intPtr := func(val int) *int { return &val } + tests := []struct { obj runtime.Object sort runtime.Object @@ -71,7 +73,7 @@ func TestSortingPrinter(t *testing.T) { }, }, }, - field: "{.ObjectMeta.Name}", + field: "{.metadata.name}", }, { name: "reverse-order", @@ -113,7 +115,7 @@ func TestSortingPrinter(t *testing.T) { }, }, }, - field: "{.ObjectMeta.Name}", + field: "{.metadata.name}", }, { name: "random-order-numbers", @@ -121,17 +123,17 @@ func TestSortingPrinter(t *testing.T) { Items: []api.ReplicationController{ { Spec: api.ReplicationControllerSpec{ - Replicas: 5, + Replicas: intPtr(5), }, }, { Spec: api.ReplicationControllerSpec{ - Replicas: 1, + Replicas: intPtr(1), }, }, { Spec: api.ReplicationControllerSpec{ - Replicas: 9, + Replicas: intPtr(9), }, }, }, @@ -140,22 +142,22 @@ func TestSortingPrinter(t *testing.T) { Items: []api.ReplicationController{ { Spec: api.ReplicationControllerSpec{ - Replicas: 1, + Replicas: intPtr(1), }, }, { Spec: api.ReplicationControllerSpec{ - Replicas: 5, + Replicas: intPtr(5), }, }, { Spec: api.ReplicationControllerSpec{ - Replicas: 9, + Replicas: intPtr(9), }, }, }, }, - field: "{.Spec.Replicas}", + field: "{.spec.replicas}", }, } for _, test := range tests { diff --git a/pkg/util/jsonpath/jsonpath.go b/pkg/util/jsonpath/jsonpath.go index b924e76f5dd..817af6307e1 100644 --- a/pkg/util/jsonpath/jsonpath.go +++ b/pkg/util/jsonpath/jsonpath.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "reflect" + "strings" "k8s.io/kubernetes/third_party/golang/template" ) @@ -258,9 +259,46 @@ func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect. return result, nil } +func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (reflect.Value, error) { + t := value.Type() + var inlineValue *reflect.Value + for ix := 0; ix < t.NumField(); ix++ { + f := t.Field(ix) + jsonTag := f.Tag.Get("json") + parts := strings.Split(jsonTag, ",") + if len(parts) == 0 { + continue + } + if parts[0] == node.Value { + return value.Field(ix), nil + } + if len(parts[0]) == 0 { + val := value.Field(ix) + inlineValue = &val + } + } + if inlineValue != nil { + if inlineValue.Kind() == reflect.Struct { + // handle 'inline' + match, err := j.findFieldInValue(inlineValue, node) + if err != nil { + return reflect.Value{}, err + } + if match.IsValid() { + return match, nil + } + } + } + return value.FieldByName(node.Value), nil +} + // evalField evaluates filed of struct or key of map. func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) { results := []reflect.Value{} + // If there's no input, there's no output + if len(input) == 0 { + return results, nil + } for _, value := range input { var result reflect.Value value, isNil := template.Indirect(value) @@ -269,7 +307,10 @@ func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect. } if value.Kind() == reflect.Struct { - result = value.FieldByName(node.Value) + var err error + if result, err = j.findFieldInValue(&value, node); err != nil { + return nil, err + } } else if value.Kind() == reflect.Map { result = value.MapIndex(reflect.ValueOf(node.Value)) }