diff --git a/pkg/printers/BUILD b/pkg/printers/BUILD index 3c60890ef9b..8e6e4b4da2b 100644 --- a/pkg/printers/BUILD +++ b/pkg/printers/BUILD @@ -25,6 +25,8 @@ go_library( ], tags = ["automanaged"], deps = [ + "//pkg/util/slice:go_default_library", + "//vendor/github.com/fatih/camelcase:go_default_library", "//vendor/github.com/ghodss/yaml:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", @@ -64,3 +66,11 @@ filegroup( ], tags = ["automanaged"], ) + +go_test( + name = "go_default_test", + srcs = ["humanreadable_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library"], +) diff --git a/pkg/printers/humanreadable.go b/pkg/printers/humanreadable.go index 69aa34fa051..cce89c69487 100644 --- a/pkg/printers/humanreadable.go +++ b/pkg/printers/humanreadable.go @@ -21,15 +21,18 @@ import ( "fmt" "io" "reflect" + "sort" "strings" "text/tabwriter" + "github.com/fatih/camelcase" "github.com/golang/glog" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/util/slice" ) var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too. @@ -201,8 +204,51 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er } if _, err := meta.Accessor(obj); err == nil { + // we don't recognize this type, but we can still attempt to print some reasonable information about. + unstructured, ok := obj.(runtime.Unstructured) + if !ok { + return fmt.Errorf("error: unknown type %#v", obj) + } + + content := unstructured.UnstructuredContent() + + // we'll elect a few more fields to print depending on how much columns are already taken + maxDiscoveredFieldsToPrint := 3 + maxDiscoveredFieldsToPrint = maxDiscoveredFieldsToPrint - len(h.options.ColumnLabels) + if h.options.WithNamespace { // where's my ternary + maxDiscoveredFieldsToPrint-- + } + if h.options.ShowLabels { + maxDiscoveredFieldsToPrint-- + } + if maxDiscoveredFieldsToPrint < 0 { + maxDiscoveredFieldsToPrint = 0 + } + + var discoveredFieldNames []string // we want it predictable so this will be used to sort + ignoreIfDiscovered := []string{"kind", "apiVersion"} // these are already covered + for field, value := range content { + if slice.ContainsString(ignoreIfDiscovered, field, nil) { + continue + } + switch value.(type) { + case map[string]interface{}: + // just simpler types + continue + } + discoveredFieldNames = append(discoveredFieldNames, field) + } + sort.Strings(discoveredFieldNames) + if len(discoveredFieldNames) > maxDiscoveredFieldsToPrint { + discoveredFieldNames = discoveredFieldNames[:maxDiscoveredFieldsToPrint] + } + if !h.options.NoHeaders && t != h.lastType { headers := []string{"NAME", "KIND"} + for _, discoveredField := range discoveredFieldNames { + fieldAsHeader := strings.ToUpper(strings.Join(camelcase.Split(discoveredField), " ")) + headers = append(headers, fieldAsHeader) + } headers = append(headers, formatLabelHeaders(h.options.ColumnLabels)...) // LABELS is always the last column. headers = append(headers, formatShowLabelsHeader(h.options.ShowLabels, t)...) @@ -213,13 +259,8 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er h.lastType = t } - // we don't recognize this type, but we can still attempt to print some reasonable information about. - unstructured, ok := obj.(runtime.Unstructured) - if !ok { - return fmt.Errorf("error: unknown type %#v", obj) - } // if the error isn't nil, report the "I don't recognize this" error - if err := printUnstructured(unstructured, w, h.options); err != nil { + if err := printUnstructured(unstructured, w, discoveredFieldNames, h.options); err != nil { return err } return nil @@ -230,7 +271,7 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er } // TODO: this method assumes the meta/v1 server API, so should be refactored out of this package -func printUnstructured(unstructured runtime.Unstructured, w io.Writer, options PrintOptions) error { +func printUnstructured(unstructured runtime.Unstructured, w io.Writer, additionalFields []string, options PrintOptions) error { metadata, err := meta.Accessor(unstructured) if err != nil { return err @@ -258,11 +299,26 @@ func printUnstructured(unstructured runtime.Unstructured, w io.Writer, options P kind = kind + "." + version.Version + "." + version.Group } } + name := formatResourceName(options.Kind, metadata.GetName(), options.WithKind) if _, err := fmt.Fprintf(w, "%s\t%s", name, kind); err != nil { return err } + for _, field := range additionalFields { + if value, ok := content[field]; ok { + var formattedValue string + switch typedValue := value.(type) { + case []interface{}: + formattedValue = fmt.Sprintf("%d item(s)", len(typedValue)) + default: + formattedValue = fmt.Sprintf("%v", value) + } + if _, err := fmt.Fprintf(w, "\t%s", formattedValue); err != nil { + return err + } + } + } if _, err := fmt.Fprint(w, appendLabels(metadata.GetLabels(), options.ColumnLabels)); err != nil { return err } diff --git a/pkg/printers/humanreadable_test.go b/pkg/printers/humanreadable_test.go new file mode 100644 index 00000000000..2649ba34e23 --- /dev/null +++ b/pkg/printers/humanreadable_test.go @@ -0,0 +1,90 @@ +/* +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 printers + +import ( + "bytes" + "regexp" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestPrintUnstructuredObject(t *testing.T) { + tests := []struct { + expected string + options PrintOptions + }{ + { + expected: "NAME\\s+KIND\\s+DUMMY 1\\s+DUMMY 2\\s+ITEMS\nMyName\\s+Test\\.v1\\.\\s+present\\s+present\\s+1 item\\(s\\)", + }, + { + options: PrintOptions{ + WithNamespace: true, + }, + expected: "NAMESPACE\\s+NAME\\s+KIND\\s+DUMMY 1\\s+DUMMY 2\nMyNamespace\\s+MyName\\s+Test\\.v1\\.\\s+present\\s+present", + }, + { + options: PrintOptions{ + ShowLabels: true, + WithNamespace: true, + }, + expected: "NAMESPACE\\s+NAME\\s+KIND\\s+DUMMY 1\\s+LABELS\nMyNamespace\\s+MyName\\s+Test\\.v1\\.\\s+present\\s+", + }, + } + out := bytes.NewBuffer([]byte{}) + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "dummy1": "present", + "dummy2": "present", + "metadata": map[string]interface{}{ + "name": "MyName", + "namespace": "MyNamespace", + "creationTimestamp": "2017-04-01T00:00:00Z", + "resourceVersion": 123, + "uid": "00000000-0000-0000-0000-000000000001", + "dummy3": "present", + }, + "items": []interface{}{ + map[string]interface{}{ + "itemBool": true, + "itemInt": 42, + }, + }, + "url": "http://localhost", + "status": "ok", + }, + } + + for _, test := range tests { + printer := &HumanReadablePrinter{ + options: test.options, + } + printer.PrintObj(obj, out) + + matches, err := regexp.MatchString(test.expected, out.String()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !matches { + t.Errorf("wanted %s, got %s", test.expected, out) + } + } +}