diff --git a/pkg/printers/internalversion/BUILD b/pkg/printers/internalversion/BUILD index b6366fd6ff..67ca8fddfb 100644 --- a/pkg/printers/internalversion/BUILD +++ b/pkg/printers/internalversion/BUILD @@ -87,6 +87,8 @@ go_library( "//pkg/kubelet/qos:go_default_library", "//pkg/printers:go_default_library", "//pkg/util/node:go_default_library", + "//pkg/util/slice:go_default_library", + "//vendor/github.com/fatih/camelcase:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", diff --git a/pkg/printers/internalversion/describe.go b/pkg/printers/internalversion/describe.go index 320c77ab25..932d17cdaa 100644 --- a/pkg/printers/internalversion/describe.go +++ b/pkg/printers/internalversion/describe.go @@ -31,6 +31,7 @@ import ( "github.com/golang/glog" + "github.com/fatih/camelcase" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" @@ -70,6 +71,7 @@ import ( "k8s.io/kubernetes/pkg/fieldpath" "k8s.io/kubernetes/pkg/kubelet/qos" "k8s.io/kubernetes/pkg/printers" + "k8s.io/kubernetes/pkg/util/slice" ) // Each level has 2 spaces for PrefixWriter @@ -198,6 +200,8 @@ func (g *genericDescriber) Describe(namespace, name string, describerSettings pr w.Write(LEVEL_0, "Name:\t%s\n", obj.GetName()) w.Write(LEVEL_0, "Namespace:\t%s\n", obj.GetNamespace()) printLabelsMultiline(w, "Labels", obj.GetLabels()) + printAnnotationsMultiline(w, "Annotations", obj.GetAnnotations()) + printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".metadata.name", ".metadata.namespace", ".metadata.labels", ".metadata.annotations") if events != nil { DescribeEvents(events, w) } @@ -205,6 +209,67 @@ func (g *genericDescriber) Describe(namespace, name string, describerSettings pr }) } +func printUnstructuredContent(w PrefixWriter, level int, content map[string]interface{}, skipPrefix string, skip ...string) { + fields := []string{} + for field := range content { + fields = append(fields, field) + } + sort.Strings(fields) + + for _, field := range fields { + value := content[field] + switch typedValue := value.(type) { + case map[string]interface{}: + skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field) + if slice.ContainsString(skip, skipExpr, nil) { + continue + } + w.Write(level, fmt.Sprintf("%s:\n", smartLabelFor(field))) + printUnstructuredContent(w, level+1, typedValue, skipExpr, skip...) + + case []interface{}: + skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field) + if slice.ContainsString(skip, skipExpr, nil) { + continue + } + w.Write(level, fmt.Sprintf("%s:\n", smartLabelFor(field))) + for _, child := range typedValue { + switch typedChild := child.(type) { + case map[string]interface{}: + printUnstructuredContent(w, level+1, typedChild, skipExpr, skip...) + default: + w.Write(level+1, fmt.Sprintf("%v\n", typedChild)) + } + } + + default: + skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field) + if slice.ContainsString(skip, skipExpr, nil) { + continue + } + w.Write(level, fmt.Sprintf("%s:\t%v\n", smartLabelFor(field), typedValue)) + } + } +} + +func smartLabelFor(field string) string { + commonAcronyms := []string{"API", "URL", "UID", "OSB", "GUID"} + + splitted := camelcase.Split(field) + for i := 0; i < len(splitted); i++ { + part := splitted[i] + + if slice.ContainsString(commonAcronyms, strings.ToUpper(part), nil) { + part = strings.ToUpper(part) + } else { + part = strings.Title(part) + } + splitted[i] = part + } + + return strings.Join(splitted, " ") +} + // DefaultObjectDescriber can describe the default Kubernetes objects. var DefaultObjectDescriber printers.ObjectDescriber diff --git a/pkg/printers/internalversion/describe_test.go b/pkg/printers/internalversion/describe_test.go index 874f6db7ee..bd7ac29b54 100644 --- a/pkg/printers/internalversion/describe_test.go +++ b/pkg/printers/internalversion/describe_test.go @@ -27,6 +27,7 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/kubernetes/federation/apis/federation" fedfake "k8s.io/kubernetes/federation/client/clientset_generated/federation_internalclientset/fake" @@ -1313,3 +1314,87 @@ func TestPrintLabelsMultiline(t *testing.T) { } } } + +func TestDescribeUnstructuredContent(t *testing.T) { + testCases := []struct { + expected string + unexpected string + }{ + { + expected: `API Version: v1 +Dummy 2: present +Items: + Item Bool: true + Item Int: 42 +Kind: Test +Metadata: + Creation Timestamp: 2017-04-01T00:00:00Z + Name: MyName + Namespace: MyNamespace + Resource Version: 123 + UID: 00000000-0000-0000-0000-000000000001 +Status: ok +URL: http://localhost +`, + }, + { + unexpected: "\nDummy 1:\tpresent\n", + }, + { + unexpected: "Dummy 1", + }, + { + unexpected: "Dummy 3", + }, + { + unexpected: "Dummy3", + }, + { + unexpected: "dummy3", + }, + { + unexpected: "dummy 3", + }, + } + out := new(bytes.Buffer) + w := NewPrefixWriter(out) + 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", + }, + } + printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".dummy1", ".metadata.dummy3") + output := out.String() + + for _, test := range testCases { + if len(test.expected) > 0 { + if !strings.Contains(output, test.expected) { + t.Errorf("Expected to find %q in: %q", test.expected, output) + } + } + if len(test.unexpected) > 0 { + if strings.Contains(output, test.unexpected) { + t.Errorf("Didn't expect to find %q in: %q", test.unexpected, output) + } + } + } +} diff --git a/pkg/util/slice/slice.go b/pkg/util/slice/slice.go index 9520bc32ae..1b8f67c0c1 100644 --- a/pkg/util/slice/slice.go +++ b/pkg/util/slice/slice.go @@ -49,6 +49,20 @@ func ShuffleStrings(s []string) []string { return shuffled } +// ContainsString checks if a given slice of strings contains the provided string. +// If a modifier func is provided, it is called with the slice item before the comparation. +func ContainsString(slice []string, s string, modifier func(s string) string) bool { + for _, item := range slice { + if item == s { + return true + } + if modifier != nil && modifier(item) == s { + return true + } + } + return false +} + // Int64Slice attaches the methods of Interface to []int64, // sorting in increasing order. type Int64Slice []int64