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" "github.com/golang/glog"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/conversion" "k8s.io/kubernetes/pkg/conversion"
"k8s.io/kubernetes/pkg/expapi" "k8s.io/kubernetes/pkg/expapi"
"k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/labels"
@ -1335,20 +1336,26 @@ func NewJSONPathPrinter(tmpl string) (*JSONPathPrinter, error) {
// PrintObj formats the obj with the JSONPath Template. // PrintObj formats the obj with the JSONPath Template.
func (j *JSONPathPrinter) PrintObj(obj runtime.Object, w io.Writer) error { func (j *JSONPathPrinter) PrintObj(obj runtime.Object, w io.Writer) error {
data, err := json.Marshal(obj) var queryObj interface{}
if err != nil { switch obj.(type) {
return err 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 { if err := j.JSONPath.Execute(w, queryObj); err != nil {
return err
}
if err = j.JSONPath.Execute(w, out); err != nil {
fmt.Fprintf(w, "Error executing template: %v\n", err) fmt.Fprintf(w, "Error executing template: %v\n", err)
fmt.Fprintf(w, "template was:\n\t%v\n", j.rawTemplate) 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 jsonpath engine was:\n\t%#v\n", queryObj)
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, obj)
return fmt.Errorf("error executing jsonpath '%v': '%v'\n----data----\n%+v\n", j.rawTemplate, err, out)
} }
return nil return nil
} }

View File

@ -102,6 +102,13 @@ func TestPrinter(t *testing.T) {
//test inputs //test inputs
simpleTest := &TestPrintType{"foo"} simpleTest := &TestPrintType{"foo"}
podTest := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "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()) testapi, err := api.Scheme.ConvertToVersion(podTest, testapi.Version())
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) 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}}", {"test template", "template", "{{if .id}}{{.id}}{{end}}{{if .metadata.name}}{{.metadata.name}}{{end}}",
podTest, "foo"}, podTest, "foo"},
{"test jsonpath", "jsonpath", "{.metadata.name}", 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"}, {"test name", "name", "", podTest, "/foo\n"},
{"emits versioned objects", "template", "{{.kind}}", testapi, "Pod"}, {"emits versioned objects", "template", "{{.kind}}", testapi, "Pod"},
} }
@ -132,7 +141,7 @@ func TestPrinter(t *testing.T) {
t.Errorf("unexpected error: %#v", err) t.Errorf("unexpected error: %#v", err)
} }
if buf.String() != test.Expect { 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] 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 { func (r *RuntimeSort) Less(i, j int) bool {
iObj := r.objs[i] iObj := r.objs[i]
jObj := r.objs[j] jObj := r.objs[j]
@ -106,18 +123,9 @@ func (r *RuntimeSort) Less(i, j int) bool {
iField := iValues[0][0] iField := iValues[0][0]
jField := jValues[0][0] jField := jValues[0][0]
switch iField.Kind() { less, err := isLess(iField, jField)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if err != nil {
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()) glog.Fatalf("Field %s in %v is an unsortable type: %s", r.field, iObj, iField.Kind().String())
} }
// default to preserving order return less
return i < j
} }

View File

@ -20,11 +20,13 @@ import (
"reflect" "reflect"
"testing" "testing"
"k8s.io/kubernetes/pkg/api" api "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
) )
func TestSortingPrinter(t *testing.T) { func TestSortingPrinter(t *testing.T) {
intPtr := func(val int) *int { return &val }
tests := []struct { tests := []struct {
obj runtime.Object obj runtime.Object
sort runtime.Object sort runtime.Object
@ -71,7 +73,7 @@ func TestSortingPrinter(t *testing.T) {
}, },
}, },
}, },
field: "{.ObjectMeta.Name}", field: "{.metadata.name}",
}, },
{ {
name: "reverse-order", name: "reverse-order",
@ -113,7 +115,7 @@ func TestSortingPrinter(t *testing.T) {
}, },
}, },
}, },
field: "{.ObjectMeta.Name}", field: "{.metadata.name}",
}, },
{ {
name: "random-order-numbers", name: "random-order-numbers",
@ -121,17 +123,17 @@ func TestSortingPrinter(t *testing.T) {
Items: []api.ReplicationController{ Items: []api.ReplicationController{
{ {
Spec: api.ReplicationControllerSpec{ Spec: api.ReplicationControllerSpec{
Replicas: 5, Replicas: intPtr(5),
}, },
}, },
{ {
Spec: api.ReplicationControllerSpec{ Spec: api.ReplicationControllerSpec{
Replicas: 1, Replicas: intPtr(1),
}, },
}, },
{ {
Spec: api.ReplicationControllerSpec{ Spec: api.ReplicationControllerSpec{
Replicas: 9, Replicas: intPtr(9),
}, },
}, },
}, },
@ -140,22 +142,22 @@ func TestSortingPrinter(t *testing.T) {
Items: []api.ReplicationController{ Items: []api.ReplicationController{
{ {
Spec: api.ReplicationControllerSpec{ Spec: api.ReplicationControllerSpec{
Replicas: 1, Replicas: intPtr(1),
}, },
}, },
{ {
Spec: api.ReplicationControllerSpec{ Spec: api.ReplicationControllerSpec{
Replicas: 5, Replicas: intPtr(5),
}, },
}, },
{ {
Spec: api.ReplicationControllerSpec{ Spec: api.ReplicationControllerSpec{
Replicas: 9, Replicas: intPtr(9),
}, },
}, },
}, },
}, },
field: "{.Spec.Replicas}", field: "{.spec.replicas}",
}, },
} }
for _, test := range tests { for _, test := range tests {

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"io" "io"
"reflect" "reflect"
"strings"
"k8s.io/kubernetes/third_party/golang/template" "k8s.io/kubernetes/third_party/golang/template"
) )
@ -258,9 +259,46 @@ func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect.
return result, nil 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. // evalField evaluates filed of struct or key of map.
func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) { func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) {
results := []reflect.Value{} results := []reflect.Value{}
// If there's no input, there's no output
if len(input) == 0 {
return results, nil
}
for _, value := range input { for _, value := range input {
var result reflect.Value var result reflect.Value
value, isNil := template.Indirect(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 { 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 { } else if value.Kind() == reflect.Map {
result = value.MapIndex(reflect.ValueOf(node.Value)) result = value.MapIndex(reflect.ValueOf(node.Value))
} }