diff --git a/pkg/kubecfg/resource_printer.go b/pkg/kubecfg/resource_printer.go index 619ea2e985a..094e97f2c4b 100644 --- a/pkg/kubecfg/resource_printer.go +++ b/pkg/kubecfg/resource_printer.go @@ -332,7 +332,9 @@ type TemplatePrinter struct { } func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) { - t, err := template.New("output").Parse(string(tmpl)) + t, err := template.New("output"). + Funcs(template.FuncMap{"exists": exists}). + Parse(string(tmpl)) if err != nil { return nil, err } @@ -346,7 +348,7 @@ func (t *TemplatePrinter) Print(data []byte, w io.Writer) error { if err != nil { return err } - if err := t.template.Execute(w, out); err != nil { + if err := t.safeExecute(w, out); err != nil { // It is way easier to debug this stuff when it shows up in // stdout instead of just stdin. So in addition to returning // a nice error, also print useful stuff with the writer. @@ -367,3 +369,93 @@ func (t *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { } return t.Print(data, w) } + +// safeExecute tries to execute the template, but catches panics and returns an error +// should the template engine panic. +func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error { + var panicErr error + // Sorry for the double anonymous function. There's probably a clever way + // to do this that has the defer'd func setting the value to be returned, but + // that would be even less obvious. + retErr := func() error { + defer func() { + if x := recover(); x != nil { + panicErr = fmt.Errorf("caught panic: %+v", x) + } + }() + return p.template.Execute(w, obj) + }() + if panicErr != nil { + return panicErr + } + return retErr +} + +// exists returns true if it would be possible to call the index function +// with these arguments. +// +// TODO: how to document this for users? +// +// index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +func exists(item interface{}, indices ...interface{}) bool { + v := reflect.ValueOf(item) + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return false + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + default: + return false + } + if x < 0 || x >= int64(v.Len()) { + return false + } + v = v.Index(int(x)) + case reflect.Map: + if !index.IsValid() { + index = reflect.Zero(v.Type().Key()) + } + if !index.Type().AssignableTo(v.Type().Key()) { + return false + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + default: + return false + } + } + if _, isNil := indirect(v); isNil { + return false + } + return true +} + +// stolen from text/template +// indirect returns the item at the end of indirection, and a bool to indicate if it's nil. +// We indirect through pointers and empty interfaces (only) because +// non-empty interfaces have methods we might need. +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/pkg/kubecfg/resource_printer_test.go b/pkg/kubecfg/resource_printer_test.go index c57c59b5fa9..d3144b248d9 100644 --- a/pkg/kubecfg/resource_printer_test.go +++ b/pkg/kubecfg/resource_printer_test.go @@ -26,6 +26,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/ghodss/yaml" ) @@ -180,7 +182,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) { func TestTemplatePanic(t *testing.T) { tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}` - // kind is always blank in memory and set on the wire printer, err := NewTemplatePrinter([]byte(tmpl)) if err != nil { t.Fatalf("tmpl fail: %v", err) @@ -194,3 +195,111 @@ func TestTemplatePanic(t *testing.T) { t.Errorf("no debugging info was printed") } } + +func TestTemplateStrings(t *testing.T) { + // This unit tests the "exists" function as well as the template from update.sh + table := map[string]struct { + pod api.Pod + expect string + }{ + "nilInfo": {api.Pod{}, "false"}, + "emptyInfo": {api.Pod{Status: api.PodStatus{Info: api.PodInfo{}}}, "false"}, + "containerExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"update-demo": api.ContainerStatus{}}, + }, + }, + "false", + }, + "netExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"net": api.ContainerStatus{}}, + }, + }, + "false", + }, + "bothExist": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{}, + }, + }, + }, + "false", + }, + "oneValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "false", + }, + "bothValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "true", + }, + } + + // The point of this test is to verify that the below template works. If you change this + // template, you need to update hack/e2e-suite/update.sh. + tmpl := + `{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}` + useThisToDebug := ` +a: {{exists . "currentState"}} +b: {{exists . "currentState" "info"}} +c: {{exists . "currentState" "info" "update-demo"}} +d: {{exists . "currentState" "info" "update-demo" "state"}} +e: {{exists . "currentState" "info" "update-demo" "state" "running"}} +f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}` + _ = useThisToDebug // don't complain about unused var + + printer, err := NewTemplatePrinter([]byte(tmpl)) + if err != nil { + t.Fatalf("tmpl fail: %v", err) + } + + for name, item := range table { + buffer := &bytes.Buffer{} + err = printer.PrintObj(&item.pod, buffer) + if err != nil { + t.Errorf("%v: unexpected err: %v", name, err) + continue + } + if e, a := item.expect, buffer.String(); e != a { + t.Errorf("%v: expected %v, got %v", name, e, a) + } + } +} diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index cfeb994f996..797c948b443 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -390,7 +390,9 @@ type TemplatePrinter struct { } func NewTemplatePrinter(tmpl []byte, asVersion string, convertor runtime.ObjectConvertor) (*TemplatePrinter, error) { - t, err := template.New("output").Parse(string(tmpl)) + t, err := template.New("output"). + Funcs(template.FuncMap{"exists": exists}). + Parse(string(tmpl)) if err != nil { return nil, err } @@ -416,7 +418,7 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { if err := json.Unmarshal(data, &out); err != nil { return err } - if err = p.template.Execute(w, out); err != nil { + if err = p.safeExecute(w, out); err != nil { // It is way easier to debug this stuff when it shows up in // stdout instead of just stdin. So in addition to returning // a nice error, also print useful stuff with the writer. @@ -429,6 +431,27 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { return nil } +// safeExecute tries to execute the template, but catches panics and returns an error +// should the template engine panic. +func (p *TemplatePrinter) safeExecute(w io.Writer, obj interface{}) error { + var panicErr error + // Sorry for the double anonymous function. There's probably a clever way + // to do this that has the defer'd func setting the value to be returned, but + // that would be even less obvious. + retErr := func() error { + defer func() { + if x := recover(); x != nil { + panicErr = fmt.Errorf("caught panic: %+v", x) + } + }() + return p.template.Execute(w, obj) + }() + if panicErr != nil { + return panicErr + } + return retErr +} + func tabbedString(f func(io.Writer) error) (string, error) { out := new(tabwriter.Writer) buf := &bytes.Buffer{} @@ -443,3 +466,72 @@ func tabbedString(f func(io.Writer) error) (string, error) { str := string(buf.String()) return str, nil } + +// exists returns true if it would be possible to call the index function +// with these arguments. +// +// TODO: how to document this for users? +// +// index returns the result of indexing its first argument by the following +// arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each +// indexed item must be a map, slice, or array. +func exists(item interface{}, indices ...interface{}) bool { + v := reflect.ValueOf(item) + for _, i := range indices { + index := reflect.ValueOf(i) + var isNil bool + if v, isNil = indirect(v); isNil { + return false + } + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.String: + var x int64 + switch index.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x = index.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x = int64(index.Uint()) + default: + return false + } + if x < 0 || x >= int64(v.Len()) { + return false + } + v = v.Index(int(x)) + case reflect.Map: + if !index.IsValid() { + index = reflect.Zero(v.Type().Key()) + } + if !index.Type().AssignableTo(v.Type().Key()) { + return false + } + if x := v.MapIndex(index); x.IsValid() { + v = x + } else { + v = reflect.Zero(v.Type().Elem()) + } + default: + return false + } + } + if _, isNil := indirect(v); isNil { + return false + } + return true +} + +// stolen from text/template +// indirect returns the item at the end of indirection, and a bool to indicate if it's nil. +// We indirect through pointers and empty interfaces (only) because +// non-empty interfaces have methods we might need. +func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { + for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { + if v.IsNil() { + return v, true + } + if v.Kind() == reflect.Interface && v.NumMethod() > 0 { + break + } + } + return v, false +} diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go index 5e829e6569e..ef5effbe003 100644 --- a/pkg/kubectl/resource_printer_test.go +++ b/pkg/kubectl/resource_printer_test.go @@ -292,7 +292,6 @@ func TestTemplateEmitsVersionedObjects(t *testing.T) { func TestTemplatePanic(t *testing.T) { tmpl := `{{and ((index .currentState.info "update-demo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}` - // kind is always blank in memory and set on the wire printer, err := NewTemplatePrinter([]byte(tmpl), testapi.Version(), api.Scheme) if err != nil { t.Fatalf("tmpl fail: %v", err) @@ -307,6 +306,114 @@ func TestTemplatePanic(t *testing.T) { } } +func TestTemplateStrings(t *testing.T) { + // This unit tests the "exists" function as well as the template from update.sh + table := map[string]struct { + pod api.Pod + expect string + }{ + "nilInfo": {api.Pod{}, "false"}, + "emptyInfo": {api.Pod{Status: api.PodStatus{Info: api.PodInfo{}}}, "false"}, + "containerExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"update-demo": api.ContainerStatus{}}, + }, + }, + "false", + }, + "netExists": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{"net": api.ContainerStatus{}}, + }, + }, + "false", + }, + "bothExist": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{}, + }, + }, + }, + "false", + }, + "oneValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{}, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "false", + }, + "bothValid": { + api.Pod{ + Status: api.PodStatus{ + Info: api.PodInfo{ + "update-demo": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + "net": api.ContainerStatus{ + State: api.ContainerState{ + Running: &api.ContainerStateRunning{ + StartedAt: util.Time{}, + }, + }, + }, + }, + }, + }, + "true", + }, + } + + // The point of this test is to verify that the below template works. If you change this + // template, you need to update hack/e2e-suite/update.sh. + tmpl := + `{{and (exists . "currentState" "info" "update-demo" "state" "running") (exists . "currentState" "info" "net" "state" "running")}}` + useThisToDebug := ` +a: {{exists . "currentState"}} +b: {{exists . "currentState" "info"}} +c: {{exists . "currentState" "info" "update-demo"}} +d: {{exists . "currentState" "info" "update-demo" "state"}} +e: {{exists . "currentState" "info" "update-demo" "state" "running"}} +f: {{exists . "currentState" "info" "update-demo" "state" "running" "startedAt"}}` + _ = useThisToDebug // don't complain about unused var + + printer, err := NewTemplatePrinter([]byte(tmpl), "v1beta1", api.Scheme) + if err != nil { + t.Fatalf("tmpl fail: %v", err) + } + + for name, item := range table { + buffer := &bytes.Buffer{} + err = printer.PrintObj(&item.pod, buffer) + if err != nil { + t.Errorf("%v: unexpected err: %v", name, err) + continue + } + if e, a := item.expect, buffer.String(); e != a { + t.Errorf("%v: expected %v, got %v", name, e, a) + } + } +} + func TestPrinters(t *testing.T) { om := func(name string) api.ObjectMeta { return api.ObjectMeta{Name: name} } templatePrinter, err := NewTemplatePrinter([]byte("{{.name}}"), testapi.Version(), api.Scheme)