Merge pull request #46235 from droot/cmd-printer-refactor

Automatic merge from submit-queue (batch tested with PRs 46235, 44786, 46833, 46756, 46669)

Get command uses print-column extn from Openapi schema

**What this PR does / why we need it**:

Kubectl Get command now uses metadata 'x-kubernetes-print-column' from Openapi schema to display a resource. This is to enable richer experience for non-compiled types (like service catalog API resources) in Kubectl. This functionality is currently guarded by a boolean flag "use-openapi-print-columns". 

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #
fixes https://github.com/kubernetes/kubectl/issues/22

**Special notes for your reviewer**:

**Release note**:

```release-note
Get command uses OpenAPI schema to enhance display for a resource if run with flag 'use-openapi-print-columns'. 
An example command:
kubectl get pods --use-openapi-print-columns 
```
This commit is contained in:
Kubernetes Submit Queue 2017-06-07 00:27:45 -07:00 committed by GitHub
commit a42867f194
18 changed files with 309 additions and 89 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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()

View File

@ -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{},

View File

@ -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

View File

@ -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
}

View File

@ -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"

View File

@ -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.")

View File

@ -12,6 +12,7 @@ go_library(
name = "go_default_library",
srcs = [
"doc.go",
"extensions.go",
"openapi.go",
"openapi_cache.go",
"openapi_getter.go",

View File

@ -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)
}

View File

@ -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.

View File

@ -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")
// 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) {
// 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"
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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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{}, "<no value>", ""},
{"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{}, "<no value>", ""},
{"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)
}

View File

@ -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 {