From ec672ca2794165d113cea8fa5afcc0cb9958afe1 Mon Sep 17 00:00:00 2001 From: juanvallejo Date: Fri, 16 Mar 2018 18:39:38 -0400 Subject: [PATCH] wire through template PrintFlags --- pkg/printers/BUILD | 5 + pkg/printers/internalversion/printers_test.go | 10 +- pkg/printers/jsonpath_flags.go | 118 ++++++++++ pkg/printers/jsonpath_flags_test.go | 212 ++++++++++++++++++ pkg/printers/kube_template_flags.go | 70 ++++++ pkg/printers/printers.go | 62 ++--- pkg/printers/template.go | 20 +- pkg/printers/template_flags.go | 123 ++++++++++ pkg/printers/template_flags_test.go | 206 +++++++++++++++++ pkg/printers/template_test.go | 2 +- 10 files changed, 766 insertions(+), 62 deletions(-) create mode 100644 pkg/printers/jsonpath_flags.go create mode 100644 pkg/printers/jsonpath_flags_test.go create mode 100644 pkg/printers/kube_template_flags.go create mode 100644 pkg/printers/template_flags.go create mode 100644 pkg/printers/template_flags_test.go diff --git a/pkg/printers/BUILD b/pkg/printers/BUILD index 318f88fc9b5..4613ac931a7 100644 --- a/pkg/printers/BUILD +++ b/pkg/printers/BUILD @@ -16,11 +16,14 @@ go_library( "json.go", "json_yaml_flags.go", "jsonpath.go", + "jsonpath_flags.go", + "kube_template_flags.go", "name.go", "name_flags.go", "printers.go", "tabwriter.go", "template.go", + "template_flags.go", "versioned.go", ], importpath = "k8s.io/kubernetes/pkg/printers", @@ -48,7 +51,9 @@ go_test( "customcolumn_flags_test.go", "customcolumn_test.go", "json_yaml_flags_test.go", + "jsonpath_flags_test.go", "name_flags_test.go", + "template_flags_test.go", ], deps = [ ":go_default_library", diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index 74a64d91741..5e5ca00ba77 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -309,7 +309,7 @@ func TestBadPrinter(t *testing.T) { }{ {"empty template", &printers.PrintOptions{OutputFormatType: "template", AllowMissingKeys: false}, fmt.Errorf("template format specified but no template given")}, {"bad template", &printers.PrintOptions{OutputFormatType: "template", OutputFormatArgument: "{{ .Name", AllowMissingKeys: false}, fmt.Errorf("error parsing template {{ .Name, template: output:1: unclosed action\n")}, - {"bad templatefile", &printers.PrintOptions{OutputFormatType: "templatefile", AllowMissingKeys: false}, fmt.Errorf("templatefile format specified but no template file given")}, + {"bad templatefile", &printers.PrintOptions{OutputFormatType: "templatefile", AllowMissingKeys: false}, fmt.Errorf("template format specified but no template given")}, {"bad jsonpath", &printers.PrintOptions{OutputFormatType: "jsonpath", OutputFormatArgument: "{.Name", AllowMissingKeys: false}, fmt.Errorf("error parsing jsonpath {.Name, unclosed action\n")}, {"unknown format", &printers.PrintOptions{OutputFormatType: "anUnknownFormat", OutputFormatArgument: "", AllowMissingKeys: false}, fmt.Errorf("output format \"anUnknownFormat\" not recognized")}, } @@ -462,7 +462,7 @@ func TestUnknownTypePrinting(t *testing.T) { func TestTemplatePanic(t *testing.T) { tmpl := `{{and ((index .currentState.info "foo").state.running.startedAt) .currentState.info.net.state.running.startedAt}}` - printer, err := printers.NewTemplatePrinter([]byte(tmpl)) + printer, err := printers.NewGoTemplatePrinter([]byte(tmpl)) if err != nil { t.Fatalf("tmpl fail: %v", err) } @@ -618,7 +618,7 @@ func TestTemplateStrings(t *testing.T) { } // The point of this test is to verify that the below template works. tmpl := `{{if (exists . "status" "containerStatuses")}}{{range .status.containerStatuses}}{{if (and (eq .name "foo") (exists . "state" "running"))}}true{{end}}{{end}}{{end}}` - p, err := printers.NewTemplatePrinter([]byte(tmpl)) + p, err := printers.NewGoTemplatePrinter([]byte(tmpl)) if err != nil { t.Fatalf("tmpl fail: %v", err) } @@ -652,13 +652,13 @@ func TestPrinters(t *testing.T) { jsonpathPrinter printers.ResourcePrinter ) - templatePrinter, err = printers.NewTemplatePrinter([]byte("{{.name}}")) + templatePrinter, err = printers.NewGoTemplatePrinter([]byte("{{.name}}")) if err != nil { t.Fatal(err) } templatePrinter = printers.NewVersionedPrinter(templatePrinter, legacyscheme.Scheme, legacyscheme.Scheme, v1.SchemeGroupVersion) - templatePrinter2, err = printers.NewTemplatePrinter([]byte("{{len .items}}")) + templatePrinter2, err = printers.NewGoTemplatePrinter([]byte("{{len .items}}")) if err != nil { t.Fatal(err) } diff --git a/pkg/printers/jsonpath_flags.go b/pkg/printers/jsonpath_flags.go new file mode 100644 index 00000000000..ba937785a0b --- /dev/null +++ b/pkg/printers/jsonpath_flags.go @@ -0,0 +1,118 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 printers + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/spf13/cobra" +) + +// JSONPathPrintFlags provides default flags necessary for template printing. +// Given the following flag values, a printer can be requested that knows +// how to handle printing based on these values. +type JSONPathPrintFlags struct { + // indicates if it is OK to ignore missing keys for rendering + // an output template. + AllowMissingKeys *bool + TemplateArgument *string +} + +// ToPrinter receives an templateFormat and returns a printer capable of +// handling --template format printing. +// Returns false if the specified templateFormat does not match a template format. +func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (ResourcePrinter, bool, error) { + if (f.TemplateArgument == nil || len(*f.TemplateArgument) == 0) && len(templateFormat) == 0 { + return nil, false, fmt.Errorf("missing --template value") + } + + templateValue := "" + + // templates are logically optional for specifying a format. + // this allows a user to specify a template format value + // as --output=jsonpath= + templateFormats := map[string]bool{ + "jsonpath": true, + "jsonpath-file": true, + } + + if f.TemplateArgument == nil || len(*f.TemplateArgument) == 0 { + for format := range templateFormats { + format = format + "=" + if strings.HasPrefix(templateFormat, format) { + templateValue = templateFormat[len(format):] + templateFormat = format[:len(format)-1] + break + } + } + } else { + templateValue = *f.TemplateArgument + } + + if _, supportedFormat := templateFormats[templateFormat]; !supportedFormat { + return nil, false, nil + } + + if len(templateValue) == 0 { + return nil, true, fmt.Errorf("template format specified but no template given") + } + + if templateFormat == "jsonpath-file" { + data, err := ioutil.ReadFile(templateValue) + if err != nil { + return nil, true, fmt.Errorf("error reading --template %s, %v\n", templateValue, err) + } + + templateValue = string(data) + } + + p, err := NewJSONPathPrinter(templateValue) + if err != nil { + return nil, true, fmt.Errorf("error parsing jsonpath %s, %v\n", templateValue, err) + } + + allowMissingKeys := true + if f.AllowMissingKeys != nil { + allowMissingKeys = *f.AllowMissingKeys + } + + p.AllowMissingKeys(allowMissingKeys) + return p, true, nil +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to template printing to it +func (f *JSONPathPrintFlags) AddFlags(c *cobra.Command) { + if f.TemplateArgument != nil { + c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when --output=jsonpath, --output=jsonpath-file.") + c.MarkFlagFilename("template") + } + if f.AllowMissingKeys != nil { + c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.") + } +} + +// NewJSONPathPrintFlags returns flags associated with +// --template printing, with default values set. +func NewJSONPathPrintFlags(templateValue string, allowMissingKeys bool) *JSONPathPrintFlags { + return &JSONPathPrintFlags{ + TemplateArgument: &templateValue, + AllowMissingKeys: &allowMissingKeys, + } +} diff --git a/pkg/printers/jsonpath_flags_test.go b/pkg/printers/jsonpath_flags_test.go new file mode 100644 index 00000000000..8a03110d5b3 --- /dev/null +++ b/pkg/printers/jsonpath_flags_test.go @@ -0,0 +1,212 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 printers_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/printers" +) + +func TestPrinterSupportsExpectedJSONPathFormats(t *testing.T) { + testObject := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + jsonpathFile, err := ioutil.TempFile("", "printers_jsonpath_flags") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer func(tempFile *os.File) { + tempFile.Close() + os.Remove(tempFile.Name()) + }(jsonpathFile) + + fmt.Fprintf(jsonpathFile, "{ .metadata.name }\n") + + testCases := []struct { + name string + outputFormat string + templateArg string + expectedError string + expectedParseError string + expectedOutput string + expectNoMatch bool + }{ + { + name: "valid output format also containing the jsonpath argument succeeds", + outputFormat: "jsonpath={ .metadata.name }", + expectedOutput: "foo", + }, + { + name: "valid output format and no --template argument results in an error", + outputFormat: "jsonpath", + expectedError: "template format specified but no template given", + }, + { + name: "valid output format and --template argument succeeds", + outputFormat: "jsonpath", + templateArg: "{ .metadata.name }", + expectedOutput: "foo", + }, + { + name: "jsonpath template file should match, and successfully return correct value", + outputFormat: "jsonpath-file", + templateArg: jsonpathFile.Name(), + expectedOutput: "foo", + }, + { + name: "valid output format and invalid --template argument results in a parsing from the printer", + outputFormat: "jsonpath", + templateArg: "{invalid}", + expectedParseError: "unrecognized identifier invalid", + }, + { + name: "no printer is matched on an invalid outputFormat", + outputFormat: "invalid", + expectNoMatch: true, + }, + { + name: "jsonpath printer should not match on any other format supported by another printer", + outputFormat: "go-template", + expectNoMatch: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + templateArg := &tc.templateArg + if len(tc.templateArg) == 0 { + templateArg = nil + } + + printFlags := printers.JSONPathPrintFlags{ + TemplateArgument: templateArg, + } + + p, matched, err := printFlags.ToPrinter(tc.outputFormat) + if tc.expectNoMatch { + if matched { + t.Fatalf("expected no printer matches for output format %q", tc.outputFormat) + } + return + } + if !matched { + t.Fatalf("expected to match template printer for output format %q", tc.outputFormat) + } + + if len(tc.expectedError) > 0 { + if err == nil || !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("expecting error %q, got %v", tc.expectedError, err) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := bytes.NewBuffer([]byte{}) + err = p.PrintObj(testObject, out) + if len(tc.expectedParseError) > 0 { + if err == nil || !strings.Contains(err.Error(), tc.expectedParseError) { + t.Errorf("expecting error %q, got %v", tc.expectedError, err) + } + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !strings.Contains(out.String(), tc.expectedOutput) { + t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String()) + } + }) + } +} + +func TestJSONPathPrinterDefaultsAllowMissingKeysToTrue(t *testing.T) { + testObject := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + allowMissingKeys := false + + testCases := []struct { + name string + templateArg string + expectedOutput string + expectedError string + allowMissingKeys *bool + }{ + { + name: "existing field does not error and returns expected value", + templateArg: "{ .metadata.name }", + expectedOutput: "foo", + allowMissingKeys: &allowMissingKeys, + }, + { + name: "missing field does not error and returns an empty string since missing keys are allowed by default", + templateArg: "{ .metadata.missing }", + expectedOutput: "", + allowMissingKeys: nil, + }, + { + name: "missing field returns expected error if field is missing and allowMissingKeys is explicitly set to false", + templateArg: "{ .metadata.missing }", + expectedError: "error executing jsonpath", + allowMissingKeys: &allowMissingKeys, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + printFlags := printers.JSONPathPrintFlags{ + TemplateArgument: &tc.templateArg, + AllowMissingKeys: tc.allowMissingKeys, + } + + outputFormat := "jsonpath" + p, matched, err := printFlags.ToPrinter(outputFormat) + if !matched { + t.Fatalf("expected to match template printer for output format %q", outputFormat) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := bytes.NewBuffer([]byte{}) + err = p.PrintObj(testObject, out) + + if len(tc.expectedError) > 0 { + if err == nil || !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("expecting error %q, got %v", tc.expectedError, err) + } + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(out.String()) != len(tc.expectedOutput) { + t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String()) + } + }) + } +} diff --git a/pkg/printers/kube_template_flags.go b/pkg/printers/kube_template_flags.go new file mode 100644 index 00000000000..eaddba7f242 --- /dev/null +++ b/pkg/printers/kube_template_flags.go @@ -0,0 +1,70 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 printers + +import "github.com/spf13/cobra" + +// KubeTemplatePrintFlags composes print flags that provide both a JSONPath and a go-template printer. +// This is necessary if dealing with cases that require support both both printers, since both sets of flags +// require overlapping flags. +type KubeTemplatePrintFlags struct { + *GoTemplatePrintFlags + *JSONPathPrintFlags + + AllowMissingKeys *bool + TemplateArgument *string +} + +func (f *KubeTemplatePrintFlags) ToPrinter(outputFormat string) (ResourcePrinter, bool, error) { + if p, match, err := f.JSONPathPrintFlags.ToPrinter(outputFormat); match { + return p, match, err + } + return f.GoTemplatePrintFlags.ToPrinter(outputFormat) +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to template printing to it +func (f *KubeTemplatePrintFlags) AddFlags(c *cobra.Command) { + if f.TemplateArgument != nil { + c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].") + c.MarkFlagFilename("template") + } + if f.AllowMissingKeys != nil { + c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.") + } +} + +// NewKubeTemplatePrintFlags returns flags associated with +// --template printing, with default values set. +func NewKubeTemplatePrintFlags() *KubeTemplatePrintFlags { + allowMissingKeysPtr := true + templateArgPtr := "" + + return &KubeTemplatePrintFlags{ + GoTemplatePrintFlags: &GoTemplatePrintFlags{ + TemplateArgument: &templateArgPtr, + AllowMissingKeys: &allowMissingKeysPtr, + }, + JSONPathPrintFlags: &JSONPathPrintFlags{ + TemplateArgument: &templateArgPtr, + AllowMissingKeys: &allowMissingKeysPtr, + }, + + TemplateArgument: &templateArgPtr, + AllowMissingKeys: &allowMissingKeysPtr, + } +} diff --git a/pkg/printers/printers.go b/pkg/printers/printers.go index 8c1474f804e..cc5e98b7174 100644 --- a/pkg/printers/printers.go +++ b/pkg/printers/printers.go @@ -18,7 +18,6 @@ package printers import ( "fmt" - "io/ioutil" "k8s.io/apimachinery/pkg/runtime" ) @@ -57,57 +56,28 @@ func GetStandardPrinter(typer runtime.ObjectTyper, encoder runtime.Encoder, deco printer = namePrinter - case "template", "go-template": - if len(formatArgument) == 0 { - return nil, fmt.Errorf("template format specified but no template given") + case "template", "go-template", "jsonpath", "templatefile", "go-template-file", "jsonpath-file": + // TODO: construct and bind this separately (at the command level) + kubeTemplateFlags := KubeTemplatePrintFlags{ + GoTemplatePrintFlags: &GoTemplatePrintFlags{ + AllowMissingKeys: &allowMissingTemplateKeys, + TemplateArgument: &formatArgument, + }, + JSONPathPrintFlags: &JSONPathPrintFlags{ + AllowMissingKeys: &allowMissingTemplateKeys, + TemplateArgument: &formatArgument, + }, } - templatePrinter, err := NewTemplatePrinter([]byte(formatArgument)) - if err != nil { - return nil, fmt.Errorf("error parsing template %s, %v\n", formatArgument, err) - } - templatePrinter.AllowMissingKeys(allowMissingTemplateKeys) - printer = templatePrinter - case "templatefile", "go-template-file": - if len(formatArgument) == 0 { - return nil, fmt.Errorf("templatefile format specified but no template file given") + kubeTemplatePrinter, matched, err := kubeTemplateFlags.ToPrinter(format) + if !matched { + return nil, fmt.Errorf("unable to match a template printer to handle current print options") } - data, err := ioutil.ReadFile(formatArgument) if err != nil { - return nil, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) + return nil, err } - templatePrinter, err := NewTemplatePrinter(data) - if err != nil { - return nil, fmt.Errorf("error parsing template %s, %v\n", string(data), err) - } - templatePrinter.AllowMissingKeys(allowMissingTemplateKeys) - printer = templatePrinter - case "jsonpath": - if len(formatArgument) == 0 { - return nil, fmt.Errorf("jsonpath template format specified but no template given") - } - jsonpathPrinter, err := NewJSONPathPrinter(formatArgument) - if err != nil { - return nil, fmt.Errorf("error parsing jsonpath %s, %v\n", formatArgument, err) - } - jsonpathPrinter.AllowMissingKeys(allowMissingTemplateKeys) - printer = jsonpathPrinter - - case "jsonpath-file": - if len(formatArgument) == 0 { - return nil, fmt.Errorf("jsonpath file format specified but no template file given") - } - data, err := ioutil.ReadFile(formatArgument) - if err != nil { - return nil, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) - } - jsonpathPrinter, err := NewJSONPathPrinter(string(data)) - if err != nil { - return nil, fmt.Errorf("error parsing template %s, %v\n", string(data), err) - } - jsonpathPrinter.AllowMissingKeys(allowMissingTemplateKeys) - printer = jsonpathPrinter + printer = kubeTemplatePrinter case "custom-columns", "custom-columns-file": customColumnsFlags := &CustomColumnsPrintFlags{ diff --git a/pkg/printers/template.go b/pkg/printers/template.go index f95d50b4379..efc4ed2cba2 100644 --- a/pkg/printers/template.go +++ b/pkg/printers/template.go @@ -26,13 +26,13 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -// TemplatePrinter is an implementation of ResourcePrinter which formats data with a Go Template. -type TemplatePrinter struct { +// GoTemplatePrinter is an implementation of ResourcePrinter which formats data with a Go Template. +type GoTemplatePrinter struct { rawTemplate string template *template.Template } -func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) { +func NewGoTemplatePrinter(tmpl []byte) (*GoTemplatePrinter, error) { t, err := template.New("output"). Funcs(template.FuncMap{ "exists": exists, @@ -42,14 +42,14 @@ func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) { if err != nil { return nil, err } - return &TemplatePrinter{ + return &GoTemplatePrinter{ rawTemplate: string(tmpl), template: t, }, nil } // AllowMissingKeys tells the template engine if missing keys are allowed. -func (p *TemplatePrinter) AllowMissingKeys(allow bool) { +func (p *GoTemplatePrinter) AllowMissingKeys(allow bool) { if allow { p.template.Option("missingkey=default") } else { @@ -57,12 +57,12 @@ func (p *TemplatePrinter) AllowMissingKeys(allow bool) { } } -func (p *TemplatePrinter) AfterPrint(w io.Writer, res string) error { +func (p *GoTemplatePrinter) AfterPrint(w io.Writer, res string) error { return nil } // PrintObj formats the obj with the Go Template. -func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { +func (p *GoTemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { var data []byte var err error data, err = json.Marshal(obj) @@ -88,17 +88,17 @@ func (p *TemplatePrinter) PrintObj(obj runtime.Object, w io.Writer) error { } // TODO: implement HandledResources() -func (p *TemplatePrinter) HandledResources() []string { +func (p *GoTemplatePrinter) HandledResources() []string { return []string{} } -func (p *TemplatePrinter) IsGeneric() bool { +func (p *GoTemplatePrinter) IsGeneric() bool { return true } // 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 { +func (p *GoTemplatePrinter) 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 diff --git a/pkg/printers/template_flags.go b/pkg/printers/template_flags.go new file mode 100644 index 00000000000..c016d0520d9 --- /dev/null +++ b/pkg/printers/template_flags.go @@ -0,0 +1,123 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 printers + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/spf13/cobra" +) + +// GoTemplatePrintFlags provides default flags necessary for template printing. +// Given the following flag values, a printer can be requested that knows +// how to handle printing based on these values. +type GoTemplatePrintFlags struct { + // indicates if it is OK to ignore missing keys for rendering + // an output template. + AllowMissingKeys *bool + TemplateArgument *string +} + +// ToPrinter receives an templateFormat and returns a printer capable of +// handling --template format printing. +// Returns false if the specified templateFormat does not match a template format. +func (f *GoTemplatePrintFlags) ToPrinter(templateFormat string) (ResourcePrinter, bool, error) { + if (f.TemplateArgument == nil || len(*f.TemplateArgument) == 0) && len(templateFormat) == 0 { + return nil, false, fmt.Errorf("missing --template argument") + } + + templateValue := "" + + // templates are logically optional for specifying a format. + // this allows a user to specify a template format value + // as --output=go-template= + supportedFormats := map[string]bool{ + "template": true, + "go-template": true, + "go-template-file": true, + "templatefile": true, + } + + if f.TemplateArgument == nil || len(*f.TemplateArgument) == 0 { + for format := range supportedFormats { + format = format + "=" + if strings.HasPrefix(templateFormat, format) { + templateValue = templateFormat[len(format):] + templateFormat = format[:len(format)-1] + break + } + } + } else { + templateValue = *f.TemplateArgument + } + + if _, supportedFormat := supportedFormats[templateFormat]; !supportedFormat { + return nil, false, nil + } + + if len(templateValue) == 0 { + return nil, true, fmt.Errorf("template format specified but no template given") + } + + if templateFormat == "templatefile" || templateFormat == "go-template-file" { + data, err := ioutil.ReadFile(templateValue) + if err != nil { + return nil, true, fmt.Errorf("error reading --template %s, %v\n", templateValue, err) + } + + templateValue = string(data) + } + + p, err := NewGoTemplatePrinter([]byte(templateValue)) + if err != nil { + return nil, true, fmt.Errorf("error parsing template %s, %v\n", templateValue, err) + } + + allowMissingKeys := true + if f.AllowMissingKeys != nil { + allowMissingKeys = *f.AllowMissingKeys + } + + p.AllowMissingKeys(allowMissingKeys) + return p, true, nil +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to template printing to it +func (f *GoTemplatePrintFlags) AddFlags(c *cobra.Command) { + if f.TemplateArgument != nil { + c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview].") + c.MarkFlagFilename("template") + } + if f.AllowMissingKeys != nil { + c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.") + } +} + +// NewGoTemplatePrintFlags returns flags associated with +// --template printing, with default values set. +func NewGoTemplatePrintFlags() *GoTemplatePrintFlags { + allowMissingKeysPtr := true + templateValuePtr := "" + + return &GoTemplatePrintFlags{ + TemplateArgument: &templateValuePtr, + AllowMissingKeys: &allowMissingKeysPtr, + } +} diff --git a/pkg/printers/template_flags_test.go b/pkg/printers/template_flags_test.go new file mode 100644 index 00000000000..bce5b2380bc --- /dev/null +++ b/pkg/printers/template_flags_test.go @@ -0,0 +1,206 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 printers_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/printers" +) + +func TestPrinterSupportsExpectedTemplateFormats(t *testing.T) { + testObject := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + templateFile, err := ioutil.TempFile("", "printers_jsonpath_flags") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer func(tempFile *os.File) { + tempFile.Close() + os.Remove(tempFile.Name()) + }(templateFile) + + fmt.Fprintf(templateFile, "{{ .metadata.name }}") + + testCases := []struct { + name string + outputFormat string + templateArg string + expectedError string + expectedParseError string + expectedOutput string + expectNoMatch bool + }{ + { + name: "valid output format also containing the template argument succeeds", + outputFormat: "go-template={{ .metadata.name }}", + expectedOutput: "foo", + }, + { + name: "valid output format and no template argument results in an error", + outputFormat: "template", + expectedError: "template format specified but no template given", + }, + { + name: "valid output format and template argument succeeds", + outputFormat: "go-template", + templateArg: "{{ .metadata.name }}", + expectedOutput: "foo", + }, + { + name: "Go-template file should match, and successfully return correct value", + outputFormat: "go-template-file", + templateArg: templateFile.Name(), + expectedOutput: "foo", + }, + { + name: "valid output format and invalid template argument results in the templateArg contents as the output", + outputFormat: "go-template", + templateArg: "invalid", + expectedOutput: "invalid", + }, + { + name: "no printer is matched on an invalid outputFormat", + outputFormat: "invalid", + expectNoMatch: true, + }, + { + name: "go-template printer should not match on any other format supported by another printer", + outputFormat: "jsonpath", + expectNoMatch: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + templateArg := &tc.templateArg + if len(tc.templateArg) == 0 { + templateArg = nil + } + + printFlags := printers.GoTemplatePrintFlags{ + TemplateArgument: templateArg, + } + + p, matched, err := printFlags.ToPrinter(tc.outputFormat) + if tc.expectNoMatch { + if matched { + t.Fatalf("expected no printer matches for output format %q", tc.outputFormat) + } + return + } + if !matched { + t.Fatalf("expected to match template printer for output format %q", tc.outputFormat) + } + + if len(tc.expectedError) > 0 { + if err == nil || !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("expecting error %q, got %v", tc.expectedError, err) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := bytes.NewBuffer([]byte{}) + err = p.PrintObj(testObject, out) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(out.String()) != len(tc.expectedOutput) { + t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String()) + } + }) + } +} + +func TestTemplatePrinterDefaultsAllowMissingKeysToTrue(t *testing.T) { + testObject := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + allowMissingKeys := false + + testCases := []struct { + name string + templateArg string + expectedOutput string + expectedError string + allowMissingKeys *bool + }{ + { + name: "existing field does not error and returns expected value", + templateArg: "{{ .metadata.name }}", + expectedOutput: "foo", + allowMissingKeys: &allowMissingKeys, + }, + { + name: "missing field does not error and returns no value since missing keys are allowed by default", + templateArg: "{{ .metadata.missing }}", + expectedOutput: "", + allowMissingKeys: nil, + }, + { + name: "missing field returns expected error if field is missing and allowMissingKeys is explicitly set to false", + templateArg: "{{ .metadata.missing }}", + expectedError: "error executing template", + allowMissingKeys: &allowMissingKeys, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + printFlags := printers.GoTemplatePrintFlags{ + TemplateArgument: &tc.templateArg, + AllowMissingKeys: tc.allowMissingKeys, + } + + outputFormat := "template" + p, matched, err := printFlags.ToPrinter(outputFormat) + if !matched { + t.Fatalf("expected to match template printer for output format %q", outputFormat) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := bytes.NewBuffer([]byte{}) + err = p.PrintObj(testObject, out) + + if len(tc.expectedError) > 0 { + if err == nil || !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("expecting error %q, got %v", tc.expectedError, err) + } + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if len(out.String()) != len(tc.expectedOutput) { + t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String()) + } + }) + } +} diff --git a/pkg/printers/template_test.go b/pkg/printers/template_test.go index 43526fd611d..d25869fe80b 100644 --- a/pkg/printers/template_test.go +++ b/pkg/printers/template_test.go @@ -53,7 +53,7 @@ func TestTemplate(t *testing.T) { for name, test := range testCase { buffer := &bytes.Buffer{} - p, err := NewTemplatePrinter([]byte(test.template)) + p, err := NewGoTemplatePrinter([]byte(test.template)) if err != nil { if test.expectErr == nil { t.Errorf("[%s]expected success but got:\n %v\n", name, err)