diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 07e4c9f9b4c..65faf73cde2 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -1045,6 +1045,33 @@ run_kubectl_get_tests() { kube::test::if_has_string "${output_message}" "/apis/batch/v1/namespaces/default/jobs 200 OK" kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/deployments 200 OK" kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/replicasets 200 OK" + + ### Test --allow-missing-template-keys + # Pre-condition: no POD exists + create_and_use_new_namespace + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' + # Command + kubectl create -f test/fixtures/doc-yaml/admin/limitrange/valid-pod.yaml "${kube_flags[@]}" + # Post-condition: valid-pod POD is created + kubectl get "${kube_flags[@]}" pods -o json + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' + + ## check --allow-missing-template-keys defaults to true for jsonpath templates + kubectl get "${kube_flags[@]}" pod valid-pod -o jsonpath='{.missing}' + + ## check --allow-missing-template-keys defaults to true for go templates + kubectl get "${kube_flags[@]}" pod valid-pod -o go-template='{{.missing}}' + + ## check --allow-missing-template-keys=false results in an error for a missing key with jsonpath + output_message=$(! kubectl get pod valid-pod --allow-missing-template-keys=false -o jsonpath='{.missing}' "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" 'error executing jsonpath "{.missing}": missing is not found' + + ## check --allow-missing-template-keys=false results in an error for a missing key with go + output_message=$(! kubectl get pod valid-pod --allow-missing-template-keys=false -o go-template='{{.missing}}' "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" 'error executing template "{{.missing}}": template: output:1:2: executing "output" at <.missing>: map has no entry for key "missing"' + + # cleanup + kubectl delete pods valid-pod "${kube_flags[@]}" } run_kubectl_request_timeout_tests() { diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 3b25ad6c185..a5b2678fe98 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -7,6 +7,7 @@ advertised-address algorithm-provider all-namespaces allocate-node-cidrs +allow-missing-template-keys allow-privileged allowed-not-ready-nodes anonymous-auth diff --git a/pkg/kubectl/cmd/clusterinfo_dump.go b/pkg/kubectl/cmd/clusterinfo_dump.go index 61824ff2264..9af6ab0bd97 100644 --- a/pkg/kubectl/cmd/clusterinfo_dump.go +++ b/pkg/kubectl/cmd/clusterinfo_dump.go @@ -91,7 +91,7 @@ func dumpClusterInfo(f cmdutil.Factory, cmd *cobra.Command, args []string, out i return err } - printer, _, err := kubectl.GetPrinter("json", "", false) + printer, _, err := kubectl.GetPrinter("json", "", false, true) if err != nil { return err } diff --git a/pkg/kubectl/cmd/convert.go b/pkg/kubectl/cmd/convert.go index 892c8db747a..46055cbfba2 100644 --- a/pkg/kubectl/cmd/convert.go +++ b/pkg/kubectl/cmd/convert.go @@ -146,7 +146,7 @@ func (o *ConvertOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.C } } o.encoder = f.JSONEncoder() - o.printer, _, err = kubectl.GetPrinter(outputFormat, templateFile, false) + o.printer, _, err = kubectl.GetPrinter(outputFormat, templateFile, false, cmdutil.GetFlagBool(cmd, "allow-missing-template-keys")) if err != nil { return err } diff --git a/pkg/kubectl/cmd/util/printing.go b/pkg/kubectl/cmd/util/printing.go index 74ccfb332aa..553153012a6 100644 --- a/pkg/kubectl/cmd/util/printing.go +++ b/pkg/kubectl/cmd/util/printing.go @@ -49,6 +49,7 @@ func AddOutputFlagsForMutation(cmd *cobra.Command) { // AddOutputFlags adds output related flags to a command. func AddOutputFlags(cmd *cobra.Command) { cmd.Flags().StringP("output", "o", "", "Output format. One of: json|yaml|wide|name|custom-columns=...|custom-columns-file=...|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See custom columns [http://kubernetes.io/docs/user-guide/kubectl-overview/#custom-columns], golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://kubernetes.io/docs/user-guide/jsonpath].") + cmd.Flags().Bool("allow-missing-template-keys", true, "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.") } // AddNoHeadersFlags adds no-headers flags to a command. @@ -126,7 +127,13 @@ func PrinterForCommand(cmd *cobra.Command) (kubectl.ResourcePrinter, bool, error } } - printer, generic, err := kubectl.GetPrinter(outputFormat, templateFile, GetFlagBool(cmd, "no-headers")) + // this function may be invoked by a command that did not call AddPrinterFlags first, so we need + // to be safe about how we access the allow-missing-template-keys flag + allowMissingTemplateKeys := false + if cmd.Flags().Lookup("allow-missing-template-keys") != nil { + allowMissingTemplateKeys = GetFlagBool(cmd, "allow-missing-template-keys") + } + printer, generic, err := kubectl.GetPrinter(outputFormat, templateFile, GetFlagBool(cmd, "no-headers"), allowMissingTemplateKeys) if err != nil { return nil, generic, err } diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index ce923790def..62b68064d13 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -71,7 +71,7 @@ const ( // is agnostic to schema versions, so you must send arguments to PrintObj in the // version you wish them to be shown using a VersionedPrinter (typically when // generic is true). -func GetPrinter(format, formatArgument string, noHeaders bool) (ResourcePrinter, bool, error) { +func GetPrinter(format, formatArgument string, noHeaders, allowMissingTemplateKeys bool) (ResourcePrinter, bool, error) { var printer ResourcePrinter switch format { case "json": @@ -88,11 +88,12 @@ func GetPrinter(format, formatArgument string, noHeaders bool) (ResourcePrinter, if len(formatArgument) == 0 { return nil, false, fmt.Errorf("template format specified but no template given") } - var err error - printer, err = NewTemplatePrinter([]byte(formatArgument)) + templatePrinter, err := NewTemplatePrinter([]byte(formatArgument)) if err != nil { return nil, false, 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, false, fmt.Errorf("templatefile format specified but no template file given") @@ -101,19 +102,22 @@ func GetPrinter(format, formatArgument string, noHeaders bool) (ResourcePrinter, if err != nil { return nil, false, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) } - printer, err = NewTemplatePrinter(data) + templatePrinter, err := NewTemplatePrinter(data) if err != nil { return nil, false, fmt.Errorf("error parsing template %s, %v\n", string(data), err) } + templatePrinter.AllowMissingKeys(allowMissingTemplateKeys) + printer = templatePrinter case "jsonpath": if len(formatArgument) == 0 { return nil, false, fmt.Errorf("jsonpath template format specified but no template given") } - var err error - printer, err = NewJSONPathPrinter(formatArgument) + jsonpathPrinter, err := NewJSONPathPrinter(formatArgument) if err != nil { return nil, false, fmt.Errorf("error parsing jsonpath %s, %v\n", formatArgument, err) } + jsonpathPrinter.AllowMissingKeys(allowMissingTemplateKeys) + printer = jsonpathPrinter case "jsonpath-file": if len(formatArgument) == 0 { return nil, false, fmt.Errorf("jsonpath file format specified but no template file file given") @@ -122,10 +126,12 @@ func GetPrinter(format, formatArgument string, noHeaders bool) (ResourcePrinter, if err != nil { return nil, false, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) } - printer, err = NewJSONPathPrinter(string(data)) + jsonpathPrinter, err := NewJSONPathPrinter(string(data)) if err != nil { return nil, false, fmt.Errorf("error parsing template %s, %v\n", string(data), err) } + jsonpathPrinter.AllowMissingKeys(allowMissingTemplateKeys) + printer = jsonpathPrinter case "custom-columns": var err error if printer, err = NewCustomColumnsPrinterFromSpec(formatArgument, api.Codecs.UniversalDecoder(), noHeaders); err != nil { @@ -2527,6 +2533,15 @@ func NewTemplatePrinter(tmpl []byte) (*TemplatePrinter, error) { }, nil } +// AllowMissingKeys tells the template engine if missing keys are allowed. +func (p *TemplatePrinter) AllowMissingKeys(allow bool) { + if allow { + p.template.Option("missingkey=default") + } else { + p.template.Option("missingkey=error") + } +} + func (p *TemplatePrinter) AfterPrint(w io.Writer, res string) error { return nil } diff --git a/pkg/kubectl/resource_printer_test.go b/pkg/kubectl/resource_printer_test.go index b4d77775a14..68d291336d2 100644 --- a/pkg/kubectl/resource_printer_test.go +++ b/pkg/kubectl/resource_printer_test.go @@ -78,7 +78,7 @@ func TestVersionedPrinter(t *testing.T) { } func TestPrintDefault(t *testing.T) { - printer, found, err := GetPrinter("", "", false) + printer, found, err := GetPrinter("", "", false, false) if err != nil { t.Fatalf("unexpected error: %#v", err) } @@ -133,7 +133,7 @@ func TestPrinter(t *testing.T) { } for _, test := range printerTests { buf := bytes.NewBuffer([]byte{}) - printer, generic, err := GetPrinter(test.Format, test.FormatArgument, false) + printer, generic, err := GetPrinter(test.Format, test.FormatArgument, false, true) if err != nil { t.Errorf("in %s, unexpected error: %#v", test.Name, err) } @@ -163,7 +163,7 @@ func TestBadPrinter(t *testing.T) { {"bad jsonpath", "jsonpath", "{.Name", fmt.Errorf("error parsing jsonpath {.Name, unclosed action\n")}, } for _, test := range badPrinterTests { - _, _, err := GetPrinter(test.Format, test.FormatArgument, false) + _, _, err := GetPrinter(test.Format, test.FormatArgument, false, false) if err == nil || err.Error() != test.Error.Error() { t.Errorf("in %s, expect %s, got %s", test.Name, test.Error, err) } @@ -356,7 +356,7 @@ func TestNamePrinter(t *testing.T) { }, "pod/foo\npod/bar\n"}, } - printer, _, _ := GetPrinter("name", "", false) + printer, _, _ := GetPrinter("name", "", false, false) for name, item := range tests { buff := &bytes.Buffer{} err := printer.PrintObj(item.obj, buff) @@ -1706,3 +1706,43 @@ func TestPrintPodDisruptionBudget(t *testing.T) { buf.Reset() } } + +func TestAllowMissingKeys(t *testing.T) { + tests := []struct { + Name string + AllowMissingTemplateKeys bool + Format string + Template string + Input runtime.Object + Expect string + Error string + }{ + {"test template, allow missing keys", true, "template", "{{.blarg}}", &api.Pod{}, "", ""}, + {"test template, strict", false, "template", "{{.blarg}}", &api.Pod{}, "", `error executing template "{{.blarg}}": template: output:1:2: executing "output" at <.blarg>: map has no entry for key "blarg"`}, + {"test jsonpath, allow missing keys", true, "jsonpath", "{.blarg}", &api.Pod{}, "", ""}, + {"test jsonpath, strict", false, "jsonpath", "{.blarg}", &api.Pod{}, "", "error executing jsonpath \"{.blarg}\": blarg is not found\n"}, + } + for _, test := range tests { + buf := bytes.NewBuffer([]byte{}) + printer, _, err := GetPrinter(test.Format, test.Template, false, test.AllowMissingTemplateKeys) + if err != nil { + t.Errorf("in %s, unexpected error: %#v", test.Name, err) + } + err = printer.PrintObj(test.Input, buf) + if len(test.Error) == 0 && err != nil { + t.Errorf("in %s, unexpected error: %v", test.Name, err) + continue + } + if len(test.Error) > 0 { + if err == nil { + t.Errorf("in %s, expected to get error: %v", test.Name, test.Error) + } else if e, a := test.Error, err.Error(); e != a { + t.Errorf("in %s, expected error %q, got %q", test.Name, e, a) + } + continue + } + if buf.String() != test.Expect { + t.Errorf("in %s, expect %q, got %q", test.Name, test.Expect, buf.String()) + } + } +}