diff --git a/pkg/printers/BUILD b/pkg/printers/BUILD index a2dca85be43..318f88fc9b5 100644 --- a/pkg/printers/BUILD +++ b/pkg/printers/BUILD @@ -10,6 +10,7 @@ go_library( name = "go_default_library", srcs = [ "customcolumn.go", + "customcolumn_flags.go", "humanreadable.go", "interface.go", "json.go", @@ -44,6 +45,7 @@ go_library( go_test( name = "go_default_xtest", srcs = [ + "customcolumn_flags_test.go", "customcolumn_test.go", "json_yaml_flags_test.go", "name_flags_test.go", diff --git a/pkg/printers/customcolumn_flags.go b/pkg/printers/customcolumn_flags.go new file mode 100644 index 00000000000..e1424f5331b --- /dev/null +++ b/pkg/printers/customcolumn_flags.go @@ -0,0 +1,101 @@ +/* +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" + "os" + "strings" + + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/kubectl/scheme" +) + +// CustomColumnsPrintFlags provides default flags necessary for printing +// custom resource columns from an inline-template or file. +type CustomColumnsPrintFlags struct { + NoHeaders bool + TemplateArgument string +} + +// ToPrinter receives an templateFormat and returns a printer capable of +// handling custom-column printing. +// Returns false if the specified templateFormat does not match a supported format. +// Supported format types can be found in pkg/printers/printers.go +func (f *CustomColumnsPrintFlags) ToPrinter(templateFormat string) (ResourcePrinter, bool, error) { + if len(templateFormat) == 0 { + return nil, false, fmt.Errorf("missing output format") + } + + templateValue := "" + + supportedFormats := map[string]bool{ + "custom-columns-file": true, + "custom-columns": true, + } + + if 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("custom-columns format specified but no custom columns given") + } + + decoder := scheme.Codecs.UniversalDecoder() + + if templateFormat == "custom-columns-file" { + file, err := os.Open(templateValue) + if err != nil { + return nil, true, fmt.Errorf("error reading template %s, %v\n", templateValue, err) + } + defer file.Close() + p, err := NewCustomColumnsPrinterFromTemplate(file, decoder) + return p, true, err + } + + p, err := NewCustomColumnsPrinterFromSpec(templateValue, decoder, f.NoHeaders) + return p, true, err +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to custom-columns printing +func (f *CustomColumnsPrintFlags) AddFlags(c *cobra.Command) {} + +// NewCustomColumnsPrintFlags returns flags associated with +// custom-column printing, with default values set. +// NoHeaders and TemplateArgument should be set by callers. +func NewCustomColumnsPrintFlags(noHeaders bool, templateValue string) *CustomColumnsPrintFlags { + return &CustomColumnsPrintFlags{ + NoHeaders: noHeaders, + TemplateArgument: templateValue, + } +} diff --git a/pkg/printers/customcolumn_flags_test.go b/pkg/printers/customcolumn_flags_test.go new file mode 100644 index 00000000000..8f337ebf028 --- /dev/null +++ b/pkg/printers/customcolumn_flags_test.go @@ -0,0 +1,139 @@ +/* +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 TestPrinterSupportsExpectedCustomColumnFormats(t *testing.T) { + testObject := &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} + + customColumnsFile, 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()) + }(customColumnsFile) + + fmt.Fprintf(customColumnsFile, "NAME\n.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 custom-columns argument succeeds", + outputFormat: "custom-columns=NAME:.metadata.name", + expectedOutput: "foo", + }, + { + name: "valid output format and no --template argument results in an error", + outputFormat: "custom-columns", + expectedError: "custom-columns format specified but no custom columns given", + }, + { + name: "valid output format and --template argument succeeds", + outputFormat: "custom-columns", + templateArg: "NAME:.metadata.name", + expectedOutput: "foo", + }, + { + name: "custom-columns template file should match, and successfully return correct value", + outputFormat: "custom-columns-file", + templateArg: customColumnsFile.Name(), + expectedOutput: "foo", + }, + { + name: "valid output format and invalid --template argument results in a parsing error from the printer", + outputFormat: "custom-columns", + templateArg: "invalid", + expectedError: "unexpected custom-columns spec: invalid, expected
:", + }, + { + name: "no printer is matched on an invalid outputFormat", + outputFormat: "invalid", + expectNoMatch: true, + }, + { + name: "custom-columns 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) { + printFlags := printers.CustomColumnsPrintFlags{ + TemplateArgument: tc.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()) + } + }) + } +} diff --git a/pkg/printers/printers.go b/pkg/printers/printers.go index 95e6d577947..8c1474f804e 100644 --- a/pkg/printers/printers.go +++ b/pkg/printers/printers.go @@ -19,7 +19,6 @@ package printers import ( "fmt" "io/ioutil" - "os" "k8s.io/apimachinery/pkg/runtime" ) @@ -110,21 +109,20 @@ func GetStandardPrinter(typer runtime.ObjectTyper, encoder runtime.Encoder, deco jsonpathPrinter.AllowMissingKeys(allowMissingTemplateKeys) printer = jsonpathPrinter - case "custom-columns": - var err error - if printer, err = NewCustomColumnsPrinterFromSpec(formatArgument, decoders[0], options.NoHeaders); err != nil { + case "custom-columns", "custom-columns-file": + customColumnsFlags := &CustomColumnsPrintFlags{ + NoHeaders: options.NoHeaders, + TemplateArgument: formatArgument, + } + customColumnsPrinter, matched, err := customColumnsFlags.ToPrinter(format) + if !matched { + return nil, fmt.Errorf("unable to match a name printer to handle current print options") + } + if err != nil { return nil, err } - case "custom-columns-file": - file, err := os.Open(formatArgument) - if err != nil { - return nil, fmt.Errorf("error reading template %s, %v\n", formatArgument, err) - } - defer file.Close() - if printer, err = NewCustomColumnsPrinterFromTemplate(file, decoders[0]); err != nil { - return nil, err - } + printer = customColumnsPrinter case "wide": fallthrough