From f768a63fb03aeda184311338a5ad4ee3a1c3b097 Mon Sep 17 00:00:00 2001 From: Sunil Arora Date: Fri, 19 May 2017 14:24:39 -0700 Subject: [PATCH] Get cmd uses print-column extn from Openapi schema Get command now uses metadata x-kubernetes-print-columns, if present, in Openapi schema to format output for a resource. This functionality is guarded by a boolean flag 'use-openapi-print-columns'. --- pkg/kubectl/cmd/BUILD | 3 + pkg/kubectl/cmd/config/view.go | 2 +- pkg/kubectl/cmd/convert.go | 2 +- pkg/kubectl/cmd/get.go | 85 +++++++++++++++++- pkg/kubectl/cmd/get_test.go | 53 +++++++++++ pkg/kubectl/cmd/testing/fake.go | 24 +++-- pkg/kubectl/cmd/util/factory.go | 6 +- pkg/kubectl/cmd/util/factory_builder.go | 10 +-- .../cmd/util/factory_object_mapping.go | 2 +- pkg/kubectl/cmd/util/helpers.go | 4 + pkg/kubectl/cmd/util/openapi/BUILD | 1 + pkg/kubectl/cmd/util/openapi/extensions.go | 26 ++++++ pkg/kubectl/cmd/util/openapi/openapi.go | 2 +- pkg/kubectl/cmd/util/printing.go | 88 ++++++++++++------- pkg/printers/customcolumn.go | 7 +- pkg/printers/interface.go | 10 +++ pkg/printers/internalversion/printers_test.go | 67 +++++++------- pkg/printers/printers.go | 8 +- 18 files changed, 310 insertions(+), 90 deletions(-) create mode 100644 pkg/kubectl/cmd/util/openapi/extensions.go diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 8e8e119b5af3..97b16640dcd7 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -92,6 +92,7 @@ go_library( "//pkg/kubectl/cmd/templates:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", "//pkg/kubectl/cmd/util/editor:go_default_library", + "//pkg/kubectl/cmd/util/openapi:go_default_library", "//pkg/kubectl/metricsutil:go_default_library", "//pkg/kubectl/plugins:go_default_library", "//pkg/kubectl/resource:go_default_library", @@ -208,6 +209,7 @@ go_test( "//pkg/kubectl:go_default_library", "//pkg/kubectl/cmd/testing:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/kubectl/cmd/util/openapi:go_default_library", "//pkg/kubectl/plugins:go_default_library", "//pkg/kubectl/resource:go_default_library", "//pkg/printers:go_default_library", @@ -215,6 +217,7 @@ go_test( "//pkg/util/i18n:go_default_library", "//pkg/util/strings:go_default_library", "//pkg/util/term:go_default_library", + "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/gopkg.in/yaml.v2:go_default_library", diff --git a/pkg/kubectl/cmd/config/view.go b/pkg/kubectl/cmd/config/view.go index 9f6bf449b103..9ce170cbdc61 100644 --- a/pkg/kubectl/cmd/config/view.go +++ b/pkg/kubectl/cmd/config/view.go @@ -81,7 +81,7 @@ func NewCmdConfigView(out, errOut io.Writer, ConfigAccess clientcmd.ConfigAccess cmd.Flags().Set("output", defaultOutputFormat) } - printer, err := cmdutil.PrinterForCommand(cmd, meta.NewDefaultRESTMapper(nil, nil), latest.Scheme, nil, []runtime.Decoder{latest.Codec}, printers.PrintOptions{}) + printer, err := cmdutil.PrinterForCommand(cmd, nil, meta.NewDefaultRESTMapper(nil, nil), latest.Scheme, nil, []runtime.Decoder{latest.Codec}, printers.PrintOptions{}) cmdutil.CheckErr(err) printer = printers.NewVersionedPrinter(printer, latest.Scheme, latest.ExternalVersion) diff --git a/pkg/kubectl/cmd/convert.go b/pkg/kubectl/cmd/convert.go index c9bae684c63f..4eaf41d0e64b 100644 --- a/pkg/kubectl/cmd/convert.go +++ b/pkg/kubectl/cmd/convert.go @@ -164,7 +164,7 @@ func (o *ConvertOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.C cmd.Flags().Set("output", outputFormat) } o.encoder = f.JSONEncoder() - o.printer, err = f.PrinterForCommand(cmd, printers.PrintOptions{}) + o.printer, err = f.PrinterForCommand(cmd, nil, printers.PrintOptions{}) if err != nil { return err } diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index 8fb58296219b..9da7e674eee6 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -19,6 +19,7 @@ package cmd import ( "fmt" "io" + "strings" "github.com/golang/glog" "github.com/spf13/cobra" @@ -33,6 +34,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/templates" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/printers" "k8s.io/kubernetes/pkg/util/i18n" @@ -90,6 +92,10 @@ var ( kubectl get all`)) ) +const ( + useOpenAPIPrintColumnFlagLabel = "experimental-use-openapi-print-columns" +) + // NewCmdGet creates a command object for the generic "get" action, which // retrieves one or more resources from a server. func NewCmdGet(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Command { @@ -128,6 +134,7 @@ func NewCmdGet(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Comman cmd.Flags().BoolVar(&options.IgnoreNotFound, "ignore-not-found", false, "Treat \"resource not found\" as a successful retrieval.") cmd.Flags().StringSliceP("label-columns", "L", []string{}, "Accepts a comma separated list of labels that are going to be presented as columns. Names are case-sensitive. You can also use multiple flag options like -L label1 -L label2...") cmd.Flags().Bool("export", false, "If true, use 'export' for the resources. Exported resources are stripped of cluster-specific information.") + addOpenAPIPrintColumnFlags(cmd) usage := "identifying the resource to get from a server." cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) cmdutil.AddInclude3rdPartyFlags(cmd) @@ -219,7 +226,7 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ } info := infos[0] mapping := info.ResourceMapping() - printer, err := f.PrinterForMapping(cmd, mapping, allNamespaces) + printer, err := f.PrinterForMapping(cmd, nil, mapping, allNamespaces) if err != nil { return err } @@ -299,7 +306,7 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ return err } - printer, err := f.PrinterForCommand(cmd, printers.PrintOptions{}) + printer, err := f.PrinterForCommand(cmd, nil, printers.PrintOptions{}) if err != nil { return err } @@ -418,6 +425,8 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ var lastMapping *meta.RESTMapping w := printers.GetNewTabWriter(out) + useOpenAPIPrintColumns := cmdutil.GetFlagBool(cmd, useOpenAPIPrintColumnFlagLabel) + if resource.MultipleTypesRequested(args) || cmdutil.MustPrintWithKinds(objs, infos, sorter) { showKind = true } @@ -426,6 +435,7 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ for ix := range objs { var mapping *meta.RESTMapping var original runtime.Object + if sorter != nil { mapping = infos[sorter.OriginalPosition(ix)].Mapping original = infos[sorter.OriginalPosition(ix)].Object @@ -437,7 +447,15 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ if printer != nil { w.Flush() } - printer, err = f.PrinterForMapping(cmd, mapping, allNamespaces) + + var outputOpts *printers.OutputOptions + // if cmd does not specify output format and useOpenAPIPrintColumnFlagLabel flag is true, + // then get the default output options for this mapping from OpenAPI schema. + if !cmdSpecifiesOutputFmt(cmd) && useOpenAPIPrintColumns { + outputOpts, _ = outputOptsForMappingFromOpenAPI(f, cmdutil.GetOpenAPICacheDir(cmd), mapping) + } + + printer, err = f.PrinterForMapping(cmd, outputOpts, mapping, allNamespaces) if err != nil { if !errs.Has(err.Error()) { errs.Insert(err.Error()) @@ -498,7 +516,13 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ } continue } - if err := printer.PrintObj(decodedObj, w); err != nil { + objToPrint := decodedObj + if printer.IsGeneric() { + // use raw object as recieved from the builder when using generic + // printer instead of decodedObj + objToPrint = original + } + if err := printer.PrintObj(objToPrint, w); err != nil { if !errs.Has(err.Error()) { errs.Insert(err.Error()) allErrs = append(allErrs, err) @@ -511,6 +535,59 @@ func RunGet(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args [ return utilerrors.NewAggregate(allErrs) } +func addOpenAPIPrintColumnFlags(cmd *cobra.Command) { + cmd.Flags().Bool(useOpenAPIPrintColumnFlagLabel, false, "If true, use x-kubernetes-print-column metadata (if present) from openapi schema for displaying a resource.") + // marking it deprecated so that it is hidden from usage/help text. + cmd.Flags().MarkDeprecated(useOpenAPIPrintColumnFlagLabel, "its an experimental feature.") +} + func shouldGetNewPrinterForMapping(printer printers.ResourcePrinter, lastMapping, mapping *meta.RESTMapping) bool { return printer == nil || lastMapping == nil || mapping == nil || mapping.Resource != lastMapping.Resource } + +func cmdSpecifiesOutputFmt(cmd *cobra.Command) bool { + return cmdutil.GetFlagString(cmd, "output") != "" +} + +// outputOptsForMappingFromOpenAPI looks for the output format metatadata in the +// openapi schema and returns the output options for the mapping if found. +func outputOptsForMappingFromOpenAPI(f cmdutil.Factory, openAPIcacheDir string, mapping *meta.RESTMapping) (*printers.OutputOptions, bool) { + + // user has not specified any output format, check if OpenAPI has + // default specification to print this resource type + api, err := f.OpenAPISchema(openAPIcacheDir) + if err != nil { + // Error getting schema + return nil, false + } + // Found openapi metadata for this resource + kind, found := api.LookupResource(mapping.GroupVersionKind) + if !found { + // Kind not found, return empty columns + return nil, false + } + + columns, found := openapi.GetPrintColumns(kind.Extensions) + if !found { + // Extension not found, return empty columns + return nil, false + } + + return outputOptsFromStr(columns) +} + +// outputOptsFromStr parses the print-column metadata and generates printer.OutputOptions object. +func outputOptsFromStr(columnStr string) (*printers.OutputOptions, bool) { + if columnStr == "" { + return nil, false + } + parts := strings.SplitN(columnStr, "=", 2) + if len(parts) < 2 { + return nil, false + } + return &printers.OutputOptions{ + FmtType: parts[0], + FmtArg: parts[1], + AllowMissingKeys: true, + }, true +} diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index 48e99b9e0145..4fa573cfcdeb 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -26,6 +26,8 @@ import ( "strings" "testing" + "github.com/go-openapi/spec" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,6 +44,7 @@ import ( "k8s.io/kubernetes/pkg/api/testapi" apitesting "k8s.io/kubernetes/pkg/api/testing" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" + "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi" ) func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList) { @@ -185,6 +188,56 @@ func TestGetSchemaObject(t *testing.T) { } } +func TestGetObjectsWithOpenAPIOutputFormatPresent(t *testing.T) { + pods, _, _ := testData() + + f, tf, codec, _ := cmdtesting.NewAPIFactory() + tf.Printer = &testPrinter{} + // overide the openAPISchema function to return custom output + // for Pod type. + tf.OpenAPISchemaFunc = testOpenAPISchemaData + tf.UnstructuredClient = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: unstructuredSerializer, + Resp: &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + errBuf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdGet(f, buf, errBuf) + cmd.SetOutput(buf) + cmd.Flags().Set(useOpenAPIPrintColumnFlagLabel, "true") + cmd.Run(cmd, []string{"pods", "foo"}) + + expected := []runtime.Object{&pods.Items[0]} + verifyObjects(t, expected, tf.Printer.(*testPrinter).Objects) + + if len(buf.String()) == 0 { + t.Errorf("unexpected empty output") + } +} + +func testOpenAPISchemaData() (*openapi.Resources, error) { + return &openapi.Resources{ + GroupVersionKindToName: map[schema.GroupVersionKind]string{ + { + Version: "v1", + Kind: "Pod", + }: "io.k8s.kubernetes.pkg.api.v1.Pod", + }, + NameToDefinition: map[string]openapi.Kind{ + "io.k8s.kubernetes.pkg.api.v1.Pod": { + Name: "io.k8s.kubernetes.pkg.api.v1.Pod", + IsResource: false, + Extensions: spec.Extensions{ + "x-kubernetes-print-columns": "custom-columns=NAME:.metadata.name,RSRC:.metadata.resourceVersion", + }, + }, + }, + }, nil +} + func TestGetObjects(t *testing.T) { pods, _, _ := testData() diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index d940ec0bf437..797902741e35 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -23,7 +23,7 @@ import ( "path/filepath" "time" - "github.com/emicklei/go-restful-swagger12" + swagger "github.com/emicklei/go-restful-swagger12" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -230,6 +230,7 @@ type TestFactory struct { ClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error) UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error) + OpenAPISchemaFunc func() (*openapi.Resources, error) } type FakeFactory struct { @@ -336,7 +337,7 @@ func (f *FakeFactory) Describer(*meta.RESTMapping) (printers.Describer, error) { return f.tf.Describer, f.tf.Err } -func (f *FakeFactory) PrinterForCommand(cmd *cobra.Command, options printers.PrintOptions) (printers.ResourcePrinter, error) { +func (f *FakeFactory) PrinterForCommand(cmd *cobra.Command, outputOpts *printers.OutputOptions, options printers.PrintOptions) (printers.ResourcePrinter, error) { return f.tf.Printer, f.tf.Err } @@ -460,7 +461,7 @@ func (f *FakeFactory) PrintObject(cmd *cobra.Command, mapper meta.RESTMapper, ob return nil } -func (f *FakeFactory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) { +func (f *FakeFactory) PrinterForMapping(cmd *cobra.Command, outputOpts *printers.OutputOptions, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) { return f.tf.Printer, f.tf.Err } @@ -617,7 +618,7 @@ func (f *fakeAPIFactory) UnstructuredClientForMapping(m *meta.RESTMapping) (reso return f.tf.UnstructuredClient, f.tf.Err } -func (f *fakeAPIFactory) PrinterForCommand(cmd *cobra.Command, options printers.PrintOptions) (printers.ResourcePrinter, error) { +func (f *fakeAPIFactory) PrinterForCommand(cmd *cobra.Command, outputOpts *printers.OutputOptions, options printers.PrintOptions) (printers.ResourcePrinter, error) { return f.tf.Printer, f.tf.Err } @@ -691,14 +692,14 @@ func (f *fakeAPIFactory) PrintObject(cmd *cobra.Command, mapper meta.RESTMapper, return err } - printer, err := f.PrinterForMapping(cmd, mapping, false) + printer, err := f.PrinterForMapping(cmd, nil, mapping, false) if err != nil { return err } return printer.PrintObj(obj, out) } -func (f *fakeAPIFactory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) { +func (f *fakeAPIFactory) PrinterForMapping(cmd *cobra.Command, outputOpts *printers.OutputOptions, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) { return f.tf.Printer, f.tf.Err } @@ -712,6 +713,17 @@ func (f *fakeAPIFactory) SuggestedPodTemplateResources() []schema.GroupResource return []schema.GroupResource{} } +func (f *fakeAPIFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclaration, error) { + return nil, nil +} + +func (f *fakeAPIFactory) OpenAPISchema(cacheDir string) (*openapi.Resources, error) { + if f.tf.OpenAPISchemaFunc != nil { + return f.tf.OpenAPISchemaFunc() + } + return &openapi.Resources{}, nil +} + func NewAPIFactory() (cmdutil.Factory, *TestFactory, runtime.Codec, runtime.NegotiatedSerializer) { t := &TestFactory{ Validator: validation.NullSchema{}, diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 8c3d8f1c34f1..d8edee96d684 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -233,10 +233,12 @@ type BuilderFactory interface { // are declared on the command (see AddPrinterFlags). Returns a printer, or an error if a printer // could not be found. // TODO: Break the dependency on cmd here. - PrinterForCommand(cmd *cobra.Command, options printers.PrintOptions) (printers.ResourcePrinter, error) + PrinterForCommand(cmd *cobra.Command, outputOpts *printers.OutputOptions, options printers.PrintOptions) (printers.ResourcePrinter, error) // PrinterForMapping returns a printer suitable for displaying the provided resource type. // Requires that printer flags have been added to cmd (see AddPrinterFlags). - PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) + // Returns a printer, true if the printer is generic (is not internal), or + // an error if a printer could not be found. + PrinterForMapping(cmd *cobra.Command, outputOpts *printers.OutputOptions, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) // PrintObject prints an api object given command line flags to modify the output format PrintObject(cmd *cobra.Command, mapper meta.RESTMapper, obj runtime.Object, out io.Writer) error // One stop shopping for a Builder diff --git a/pkg/kubectl/cmd/util/factory_builder.go b/pkg/kubectl/cmd/util/factory_builder.go index 0d122843dc6e..25451f4c6114 100644 --- a/pkg/kubectl/cmd/util/factory_builder.go +++ b/pkg/kubectl/cmd/util/factory_builder.go @@ -48,7 +48,7 @@ func NewBuilderFactory(clientAccessFactory ClientAccessFactory, objectMappingFac return f } -func (f *ring2Factory) PrinterForCommand(cmd *cobra.Command, options printers.PrintOptions) (printers.ResourcePrinter, error) { +func (f *ring2Factory) PrinterForCommand(cmd *cobra.Command, outputOpts *printers.OutputOptions, options printers.PrintOptions) (printers.ResourcePrinter, error) { mapper, typer, err := f.objectMappingFactory.UnstructuredObject() if err != nil { return nil, err @@ -56,10 +56,10 @@ func (f *ring2Factory) PrinterForCommand(cmd *cobra.Command, options printers.Pr // TODO: used by the custom column implementation and the name implementation, break this dependency decoders := []runtime.Decoder{f.clientAccessFactory.Decoder(true), unstructured.UnstructuredJSONScheme} encoder := f.clientAccessFactory.JSONEncoder() - return PrinterForCommand(cmd, mapper, typer, encoder, decoders, options) + return PrinterForCommand(cmd, outputOpts, mapper, typer, encoder, decoders, options) } -func (f *ring2Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) { +func (f *ring2Factory) PrinterForMapping(cmd *cobra.Command, outputOpts *printers.OutputOptions, mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinter, error) { // Some callers do not have "label-columns" so we can't use the GetFlagStringSlice() helper columnLabel, err := cmd.Flags().GetStringSlice("label-columns") if err != nil { @@ -76,7 +76,7 @@ func (f *ring2Factory) PrinterForMapping(cmd *cobra.Command, mapping *meta.RESTM ColumnLabels: columnLabel, } - printer, err := f.PrinterForCommand(cmd, options) + printer, err := f.PrinterForCommand(cmd, outputOpts, options) if err != nil { return nil, err } @@ -132,7 +132,7 @@ func (f *ring2Factory) PrintObject(cmd *cobra.Command, mapper meta.RESTMapper, o return err } - printer, err := f.PrinterForMapping(cmd, mapping, false) + printer, err := f.PrinterForMapping(cmd, nil, mapping, false) if err != nil { return err } diff --git a/pkg/kubectl/cmd/util/factory_object_mapping.go b/pkg/kubectl/cmd/util/factory_object_mapping.go index 77f160e8ba2a..abb10d52bb21 100644 --- a/pkg/kubectl/cmd/util/factory_object_mapping.go +++ b/pkg/kubectl/cmd/util/factory_object_mapping.go @@ -27,7 +27,7 @@ import ( "sync" "time" - "github.com/emicklei/go-restful-swagger12" + swagger "github.com/emicklei/go-restful-swagger12" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 867d5c55cea3..1e9fc4478b7c 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -417,6 +417,10 @@ func AddOpenAPIFlags(cmd *cobra.Command) { cmd.MarkFlagFilename("schema-cache-dir") } +func GetOpenAPICacheDir(cmd *cobra.Command) string { + return GetFlagString(cmd, "schema-cache-dir") +} + func AddFilenameOptionFlags(cmd *cobra.Command, options *resource.FilenameOptions, usage string) { kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, "Filename, directory, or URL to files "+usage) cmd.Flags().BoolVarP(&options.Recursive, "recursive", "R", options.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.") diff --git a/pkg/kubectl/cmd/util/openapi/BUILD b/pkg/kubectl/cmd/util/openapi/BUILD index c9343aa5c494..20d3e25e2482 100644 --- a/pkg/kubectl/cmd/util/openapi/BUILD +++ b/pkg/kubectl/cmd/util/openapi/BUILD @@ -12,6 +12,7 @@ go_library( name = "go_default_library", srcs = [ "doc.go", + "extensions.go", "openapi.go", "openapi_cache.go", "openapi_getter.go", diff --git a/pkg/kubectl/cmd/util/openapi/extensions.go b/pkg/kubectl/cmd/util/openapi/extensions.go new file mode 100644 index 000000000000..40eaacead8c6 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/extensions.go @@ -0,0 +1,26 @@ +/* +Copyright 2017 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 openapi + +import "github.com/go-openapi/spec" + +const PrintColumnsKey = "x-kubernetes-print-columns" + +// GetPrintColumns looks for the open API extension for the display columns. +func GetPrintColumns(extensions spec.Extensions) (string, bool) { + return extensions.GetString(PrintColumnsKey) +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi.go b/pkg/kubectl/cmd/util/openapi/openapi.go index 7ee40945c2a0..df3d4223e6e2 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi.go +++ b/pkg/kubectl/cmd/util/openapi/openapi.go @@ -194,7 +194,7 @@ func (o *Resources) parseDefinition(name string, s spec.Schema) Kind { Fields: map[string]Type{}, } if err != nil { - glog.Warning(err) + glog.V(2).Info(err) } // Definition represents a primitive type - e.g. diff --git a/pkg/kubectl/cmd/util/printing.go b/pkg/kubectl/cmd/util/printing.go index 90817c26b4f1..8e2c0785fff4 100644 --- a/pkg/kubectl/cmd/util/printing.go +++ b/pkg/kubectl/cmd/util/printing.go @@ -105,38 +105,18 @@ func ValidateOutputArgs(cmd *cobra.Command) error { return nil } -// PrinterForCommand returns the default printer for this command. -// Requires that printer flags have been added to cmd (see AddPrinterFlags). -func PrinterForCommand(cmd *cobra.Command, mapper meta.RESTMapper, typer runtime.ObjectTyper, encoder runtime.Encoder, decoders []runtime.Decoder, options printers.PrintOptions) (printers.ResourcePrinter, error) { - outputFormat := GetFlagString(cmd, "output") - - // templates are logically optional for specifying a format. - // TODO once https://github.com/kubernetes/kubernetes/issues/12668 is fixed, this should fall back to GetFlagString - templateFile, _ := cmd.Flags().GetString("template") - if len(outputFormat) == 0 && len(templateFile) != 0 { - outputFormat = "template" +// PrinterForCommand returns the printer for the outputOptions (if given) or +// returns the default printer for the command. Requires that printer flags have +// been added to cmd (see AddPrinterFlags). +// TODO: remove the dependency on cmd object +func PrinterForCommand(cmd *cobra.Command, outputOpts *printers.OutputOptions, mapper meta.RESTMapper, typer runtime.ObjectTyper, encoder runtime.Encoder, decoders []runtime.Decoder, options printers.PrintOptions) (printers.ResourcePrinter, error) { + + if outputOpts == nil { + outputOpts = extractOutputOptions(cmd) } - templateFormat := []string{ - "go-template=", "go-template-file=", "jsonpath=", "jsonpath-file=", "custom-columns=", "custom-columns-file=", - } - for _, format := range templateFormat { - if strings.HasPrefix(outputFormat, format) { - templateFile = outputFormat[len(format):] - outputFormat = format[:len(format)-1] - } - } - - // 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, err := printers.GetStandardPrinter( - outputFormat, templateFile, GetFlagBool(cmd, "no-headers"), allowMissingTemplateKeys, - mapper, typer, encoder, decoders, options, - ) + printer, err := printers.GetStandardPrinter(outputOpts, + GetFlagBool(cmd, "no-headers"), mapper, typer, encoder, decoders, options) if err != nil { return nil, err } @@ -149,12 +129,12 @@ func PrinterForCommand(cmd *cobra.Command, mapper meta.RESTMapper, typer runtime // object passed is non-generic, it attempts to print the object using a HumanReadablePrinter. // Requires that printer flags have been added to cmd (see AddPrinterFlags). func PrintResourceInfoForCommand(cmd *cobra.Command, info *resource.Info, f Factory, out io.Writer) error { - printer, err := f.PrinterForCommand(cmd, printers.PrintOptions{}) + printer, err := f.PrinterForCommand(cmd, nil, printers.PrintOptions{}) if err != nil { return err } if !printer.IsGeneric() { - printer, err = f.PrinterForMapping(cmd, nil, false) + printer, err = f.PrinterForMapping(cmd, nil, nil, false) if err != nil { return err } @@ -162,6 +142,50 @@ func PrintResourceInfoForCommand(cmd *cobra.Command, info *resource.Info, f Fact return printer.PrintObj(info.Object, out) } +// extractOutputOptions parses printer specific commandline args and returns +// printers.OutputsOptions object. +func extractOutputOptions(cmd *cobra.Command) *printers.OutputOptions { + flags := cmd.Flags() + + var outputFormat string + if flags.Lookup("output") != nil { + outputFormat = GetFlagString(cmd, "output") + } + + // templates are logically optional for specifying a format. + // TODO once https://github.com/kubernetes/kubernetes/issues/12668 is fixed, this should fall back to GetFlagString + var templateFile string + if flags.Lookup("template") != nil { + templateFile = GetFlagString(cmd, "template") + } + if len(outputFormat) == 0 && len(templateFile) != 0 { + outputFormat = "template" + } + + templateFormats := []string{ + "go-template=", "go-template-file=", "jsonpath=", "jsonpath-file=", "custom-columns=", "custom-columns-file=", + } + for _, format := range templateFormats { + if strings.HasPrefix(outputFormat, format) { + templateFile = outputFormat[len(format):] + outputFormat = format[:len(format)-1] + } + } + + // 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 flags.Lookup("allow-missing-template-keys") != nil { + allowMissingTemplateKeys = GetFlagBool(cmd, "allow-missing-template-keys") + } + + return &printers.OutputOptions{ + FmtType: outputFormat, + FmtArg: templateFile, + AllowMissingKeys: allowMissingTemplateKeys, + } +} + func maybeWrapSortingPrinter(cmd *cobra.Command, printer printers.ResourcePrinter) printers.ResourcePrinter { sorting, err := cmd.Flags().GetString("sort-by") if err != nil { diff --git a/pkg/printers/customcolumn.go b/pkg/printers/customcolumn.go index c0676962a3c4..eec16ea18aa1 100644 --- a/pkg/printers/customcolumn.go +++ b/pkg/printers/customcolumn.go @@ -153,6 +153,9 @@ type CustomColumnsPrinter struct { Columns []Column Decoder runtime.Decoder NoHeaders bool + // lastType records type of resource printed last so that we don't repeat + // header while printing same type of resources. + lastType reflect.Type } func (s *CustomColumnsPrinter) AfterPrint(w io.Writer, res string) error { @@ -162,12 +165,14 @@ func (s *CustomColumnsPrinter) AfterPrint(w io.Writer, res string) error { func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error { w := tabwriter.NewWriter(out, columnwidth, tabwidth, padding, padding_character, flags) - if !s.NoHeaders { + t := reflect.TypeOf(obj) + if !s.NoHeaders && t != s.lastType { headers := make([]string, len(s.Columns)) for ix := range s.Columns { headers[ix] = s.Columns[ix].Header } fmt.Fprintln(w, strings.Join(headers, "\t")) + s.lastType = t } parsers := make([]*jsonpath.JSONPath, len(s.Columns)) for ix := range s.Columns { diff --git a/pkg/printers/interface.go b/pkg/printers/interface.go index ff2a609aa33e..52712158646d 100644 --- a/pkg/printers/interface.go +++ b/pkg/printers/interface.go @@ -100,3 +100,13 @@ type ErrNoDescriber struct { func (e ErrNoDescriber) Error() string { return fmt.Sprintf("no describer has been defined for %v", e.Types) } + +// OutputOptions represents resource output options which is used to generate a resource printer. +type OutputOptions struct { + // supported Format types can be found in pkg/printers/printers.go + FmtType string + FmtArg string + + // indicates if it is OK to ignore missing keys for rendering an output template. + AllowMissingKeys bool +} diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index 8d4c55e6c9b2..2448a90afc88 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -93,7 +93,7 @@ func TestPrintDefault(t *testing.T) { } for _, test := range printerTests { - printer, err := printers.GetStandardPrinter(test.Format, "", false, false, nil, nil, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) + printer, err := printers.GetStandardPrinter(&printers.OutputOptions{AllowMissingKeys: false}, false, nil, nil, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) if err != nil { t.Errorf("in %s, unexpected error: %#v", test.Name, err) } @@ -131,25 +131,24 @@ func TestPrinter(t *testing.T) { printerTests := []struct { Name string - Format string - FormatArgument string + OutputOpts *printers.OutputOptions Input runtime.Object OutputVersions []schema.GroupVersion Expect string }{ - {"test json", "json", "", simpleTest, nil, "{\n \"Data\": \"foo\"\n}\n"}, - {"test yaml", "yaml", "", simpleTest, nil, "Data: foo\n"}, - {"test template", "template", "{{if .id}}{{.id}}{{end}}{{if .metadata.name}}{{.metadata.name}}{{end}}", + {"test json", &printers.OutputOptions{FmtType: "json", AllowMissingKeys: true}, simpleTest, nil, "{\n \"Data\": \"foo\"\n}\n"}, + {"test yaml", &printers.OutputOptions{FmtType: "yaml", AllowMissingKeys: true}, simpleTest, nil, "Data: foo\n"}, + {"test template", &printers.OutputOptions{FmtType: "template", FmtArg: "{{if .id}}{{.id}}{{end}}{{if .metadata.name}}{{.metadata.name}}{{end}}", AllowMissingKeys: true}, podTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "foo"}, - {"test jsonpath", "jsonpath", "{.metadata.name}", podTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "foo"}, - {"test jsonpath list", "jsonpath", "{.items[*].metadata.name}", podListTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "foo bar"}, - {"test jsonpath empty list", "jsonpath", "{.items[*].metadata.name}", emptyListTest, []schema.GroupVersion{v1.SchemeGroupVersion}, ""}, - {"test name", "name", "", podTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "pods/foo\n"}, - {"emits versioned objects", "template", "{{.kind}}", testapi, []schema.GroupVersion{v1.SchemeGroupVersion}, "Pod"}, + {"test jsonpath", &printers.OutputOptions{FmtType: "jsonpath", FmtArg: "{.metadata.name}", AllowMissingKeys: true}, podTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "foo"}, + {"test jsonpath list", &printers.OutputOptions{FmtType: "jsonpath", FmtArg: "{.items[*].metadata.name}", AllowMissingKeys: true}, podListTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "foo bar"}, + {"test jsonpath empty list", &printers.OutputOptions{FmtType: "jsonpath", FmtArg: "{.items[*].metadata.name}", AllowMissingKeys: true}, emptyListTest, []schema.GroupVersion{v1.SchemeGroupVersion}, ""}, + {"test name", &printers.OutputOptions{FmtType: "name", AllowMissingKeys: true}, podTest, []schema.GroupVersion{v1.SchemeGroupVersion}, "pods/foo\n"}, + {"emits versioned objects", &printers.OutputOptions{FmtType: "template", FmtArg: "{{.kind}}", AllowMissingKeys: true}, testapi, []schema.GroupVersion{v1.SchemeGroupVersion}, "Pod"}, } for _, test := range printerTests { buf := bytes.NewBuffer([]byte{}) - printer, err := printers.GetStandardPrinter(test.Format, test.FormatArgument, false, true, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) + printer, err := printers.GetStandardPrinter(test.OutputOpts, false, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) if err != nil { t.Errorf("in %s, unexpected error: %#v", test.Name, err) } @@ -168,19 +167,18 @@ func TestPrinter(t *testing.T) { func TestBadPrinter(t *testing.T) { badPrinterTests := []struct { - Name string - Format string - FormatArgument string - Error error + Name string + OutputOpts *printers.OutputOptions + Error error }{ - {"empty template", "template", "", fmt.Errorf("template format specified but no template given")}, - {"bad template", "template", "{{ .Name", fmt.Errorf("error parsing template {{ .Name, template: output:1: unclosed action\n")}, - {"bad templatefile", "templatefile", "", fmt.Errorf("templatefile format specified but no template file given")}, - {"bad jsonpath", "jsonpath", "{.Name", fmt.Errorf("error parsing jsonpath {.Name, unclosed action\n")}, - {"unknown format", "anUnknownFormat", "", fmt.Errorf("output format \"anUnknownFormat\" not recognized")}, + {"empty template", &printers.OutputOptions{FmtType: "template", AllowMissingKeys: false}, fmt.Errorf("template format specified but no template given")}, + {"bad template", &printers.OutputOptions{FmtType: "template", FmtArg: "{{ .Name", AllowMissingKeys: false}, fmt.Errorf("error parsing template {{ .Name, template: output:1: unclosed action\n")}, + {"bad templatefile", &printers.OutputOptions{FmtType: "templatefile", AllowMissingKeys: false}, fmt.Errorf("templatefile format specified but no template file given")}, + {"bad jsonpath", &printers.OutputOptions{FmtType: "jsonpath", FmtArg: "{.Name", AllowMissingKeys: false}, fmt.Errorf("error parsing jsonpath {.Name, unclosed action\n")}, + {"unknown format", &printers.OutputOptions{FmtType: "anUnknownFormat", FmtArg: "", AllowMissingKeys: false}, fmt.Errorf("output format \"anUnknownFormat\" not recognized")}, } for _, test := range badPrinterTests { - _, err := printers.GetStandardPrinter(test.Format, test.FormatArgument, false, false, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) + _, err := printers.GetStandardPrinter(test.OutputOpts, false, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) if err == nil || err.Error() != test.Error.Error() { t.Errorf("in %s, expect %s, got %s", test.Name, test.Error, err) } @@ -373,7 +371,8 @@ func TestNamePrinter(t *testing.T) { }, "pods/foo\npods/bar\n"}, } - printer, _ := printers.GetStandardPrinter("name", "", false, false, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) + outputOpts := &printers.OutputOptions{FmtType: "name", AllowMissingKeys: false} + printer, _ := printers.GetStandardPrinter(outputOpts, false, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) for name, item := range tests { buff := &bytes.Buffer{} err := printer.PrintObj(item.obj, buff) @@ -2309,22 +2308,20 @@ func TestPrintPodDisruptionBudget(t *testing.T) { func TestAllowMissingKeys(t *testing.T) { tests := []struct { - Name string - AllowMissingTemplateKeys bool - Format string - Template string - Input runtime.Object - Expect string - Error string + Name string + OutputOpts *printers.OutputOptions + 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"}, + {"test template, allow missing keys", &printers.OutputOptions{FmtType: "template", FmtArg: "{{.blarg}}", AllowMissingKeys: true}, &api.Pod{}, "", ""}, + {"test template, strict", &printers.OutputOptions{FmtType: "template", FmtArg: "{{.blarg}}", AllowMissingKeys: false}, &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", &printers.OutputOptions{FmtType: "jsonpath", FmtArg: "{.blarg}", AllowMissingKeys: true}, &api.Pod{}, "", ""}, + {"test jsonpath, strict", &printers.OutputOptions{FmtType: "jsonpath", FmtArg: "{.blarg}", AllowMissingKeys: false}, &api.Pod{}, "", "error executing jsonpath \"{.blarg}\": blarg is not found\n"}, } for _, test := range tests { buf := bytes.NewBuffer([]byte{}) - printer, err := printers.GetStandardPrinter(test.Format, test.Template, false, test.AllowMissingTemplateKeys, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) + printer, err := printers.GetStandardPrinter(test.OutputOpts, false, api.Registry.RESTMapper(api.Registry.EnabledVersions()...), api.Scheme, api.Codecs.LegacyCodec(api.Registry.EnabledVersions()...), []runtime.Decoder{api.Codecs.UniversalDecoder(), unstructured.UnstructuredJSONScheme}, printers.PrintOptions{}) if err != nil { t.Errorf("in %s, unexpected error: %#v", test.Name, err) } diff --git a/pkg/printers/printers.go b/pkg/printers/printers.go index 220fa3980947..217755852936 100644 --- a/pkg/printers/printers.go +++ b/pkg/printers/printers.go @@ -29,7 +29,13 @@ import ( // a printer or an error. The printer 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 GetStandardPrinter(format, formatArgument string, noHeaders, allowMissingTemplateKeys bool, mapper meta.RESTMapper, typer runtime.ObjectTyper, encoder runtime.Encoder, decoders []runtime.Decoder, options PrintOptions) (ResourcePrinter, error) { +func GetStandardPrinter(outputOpts *OutputOptions, noHeaders bool, mapper meta.RESTMapper, typer runtime.ObjectTyper, encoder runtime.Encoder, decoders []runtime.Decoder, options PrintOptions) (ResourcePrinter, error) { + if outputOpts == nil { + return nil, fmt.Errorf("no output options specified") + } + + format, formatArgument, allowMissingTemplateKeys := outputOpts.FmtType, outputOpts.FmtArg, outputOpts.AllowMissingKeys + var printer ResourcePrinter switch format {