Add a printer that knows how to print user-defined columns

pull/6/head
Brendan Burns 2015-08-13 14:11:23 -07:00
parent ab73849437
commit de14623775
7 changed files with 302 additions and 35 deletions

View File

@ -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, "<none>\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
}

View File

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

View File

@ -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 {
var queryObj interface{}
switch obj.(type) {
case *v1.List, *api.List:
data, err := json.Marshal(obj)
if err != nil {
return err
}
out := map[string]interface{}{}
if err := json.Unmarshal(data, &out); err != nil {
queryObj = map[string]interface{}{}
if err := json.Unmarshal(data, &queryObj); err != nil {
return err
}
if err = j.JSONPath.Execute(w, out); err != nil {
default:
queryObj = obj
}
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
}

View File

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

View File

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

View File

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

View File

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