Merge pull request #48033 from smarterclayton/generic_printer

Automatic merge from submit-queue (batch tested with PRs 45467, 48091, 48033, 48498)

Refactor and simplify generic printer for unknown objects

The first two commits are part of other PRs

@kubernetes/sig-cli-pr-reviews part of the general refactoring for server side print
pull/6/head
Kubernetes Submit Queue 2017-07-05 12:37:33 -07:00 committed by GitHub
commit 1108738200
9 changed files with 317 additions and 361 deletions

View File

@ -25,8 +25,6 @@ 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/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
@ -69,11 +67,3 @@ 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"],
)

View File

@ -21,12 +21,9 @@ import (
"fmt"
"io"
"reflect"
"sort"
"strings"
"text/tabwriter"
"github.com/fatih/camelcase"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1"
@ -34,7 +31,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kubernetes/pkg/util/slice"
)
type TablePrinter interface {
@ -44,6 +40,7 @@ type TablePrinter interface {
type PrintHandler interface {
Handler(columns, columnsWithWide []string, printFunc interface{}) error
TableHandler(columns []metav1alpha1.TableColumnDefinition, printFunc interface{}) error
DefaultTableHandler(columns []metav1alpha1.TableColumnDefinition, printFunc interface{}) error
}
var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
@ -60,12 +57,13 @@ type handlerEntry struct {
// will only be printed if the object type changes. This makes it useful for printing items
// received from watches.
type HumanReadablePrinter struct {
handlerMap map[reflect.Type]*handlerEntry
options PrintOptions
lastType reflect.Type
skipTabWriter bool
encoder runtime.Encoder
decoder runtime.Decoder
handlerMap map[reflect.Type]*handlerEntry
defaultHandler *handlerEntry
options PrintOptions
lastType interface{}
skipTabWriter bool
encoder runtime.Encoder
decoder runtime.Decoder
}
var _ PrintHandler = &HumanReadablePrinter{}
@ -188,6 +186,25 @@ func (h *HumanReadablePrinter) TableHandler(columnDefinitions []metav1alpha1.Tab
return nil
}
// DefaultTableHandler registers a set of columns and a print func that is given a chance to process
// any object without an explicit handler. Only the most recently set print handler is used.
// See ValidateRowPrintHandlerFunc for required method signature.
func (h *HumanReadablePrinter) DefaultTableHandler(columnDefinitions []metav1alpha1.TableColumnDefinition, printFunc interface{}) error {
printFuncValue := reflect.ValueOf(printFunc)
if err := ValidateRowPrintHandlerFunc(printFuncValue); err != nil {
utilruntime.HandleError(fmt.Errorf("unable to register print function: %v", err))
return err
}
entry := &handlerEntry{
columnDefinitions: columnDefinitions,
printRows: true,
printFunc: printFuncValue,
}
h.defaultHandler = entry
return nil
}
// ValidateRowPrintHandlerFunc validates print handler signature.
// printFunc is the function that will be called to print an object.
// It must be of the following type:
@ -266,7 +283,7 @@ func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error {
return err
}
func (h *HumanReadablePrinter) printHeader(columnNames []string, w io.Writer) error {
func printHeader(columnNames []string, w io.Writer) error {
if _, err := fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil {
return err
}
@ -299,141 +316,24 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
obj, _ = decodeUnknownObject(obj, h.encoder, h.decoder)
}
// print with a registered handler
t := reflect.TypeOf(obj)
if handler := h.handlerMap[t]; handler != nil {
if !h.options.NoHeaders && t != h.lastType {
var headers []string
for _, column := range handler.columnDefinitions {
if column.Priority != 0 && !h.options.Wide {
continue
}
headers = append(headers, strings.ToUpper(column.Name))
}
headers = append(headers, formatLabelHeaders(h.options.ColumnLabels)...)
// LABELS is always the last column.
headers = append(headers, formatShowLabelsHeader(h.options.ShowLabels, t)...)
if h.options.WithNamespace {
headers = append(withNamespacePrefixColumns, headers...)
}
h.printHeader(headers, output)
h.lastType = t
}
if handler.printRows {
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(h.options)}
results := handler.printFunc.Call(args)
if results[1].IsNil() {
rows := results[0].Interface().([]metav1alpha1.TableRow)
for _, row := range rows {
if h.options.WithNamespace {
if obj := row.Object.Object; obj != nil {
if m, err := meta.Accessor(obj); err == nil {
fmt.Fprint(output, m.GetNamespace())
}
}
fmt.Fprint(output, "\t")
}
for i, cell := range row.Cells {
if i != 0 {
fmt.Fprint(output, "\t")
} else {
// TODO: remove this once we drop the legacy printers
if h.options.WithKind && len(h.options.Kind) > 0 {
fmt.Fprintf(output, "%s/%s", h.options.Kind, cell)
continue
}
}
fmt.Fprint(output, cell)
}
hasLabels := len(h.options.ColumnLabels) > 0
if obj := row.Object.Object; obj != nil && (hasLabels || h.options.ShowLabels) {
if m, err := meta.Accessor(obj); err == nil {
for _, value := range labelValues(m.GetLabels(), h.options) {
output.Write([]byte("\t"))
output.Write([]byte(value))
}
}
}
output.Write([]byte("\n"))
}
return nil
}
return results[1].Interface().(error)
}
// TODO: this code path is deprecated and will be removed when all handlers are row printers
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(output), reflect.ValueOf(h.options)}
resultValue := handler.printFunc.Call(args)[0]
if resultValue.IsNil() {
return nil
}
return resultValue.Interface().(error)
}
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 %T, expected unstructured in %#v", obj, h.handlerMap)
}
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)...)
if h.options.WithNamespace {
headers = append(withNamespacePrefixColumns, headers...)
}
h.printHeader(headers, output)
h.lastType = t
}
// if the error isn't nil, report the "I don't recognize this" error
if err := printUnstructured(unstructured, output, discoveredFieldNames, h.options); err != nil {
includeHeaders := h.lastType != t && !h.options.NoHeaders
if err := printRowsForHandlerEntry(output, handler, obj, h.options, includeHeaders); err != nil {
return err
}
h.lastType = t
return nil
}
// print with the default handler if set, and use the columns from the last time
if h.defaultHandler != nil {
includeHeaders := h.lastType != h.defaultHandler && !h.options.NoHeaders
if err := printRowsForHandlerEntry(output, h.defaultHandler, obj, h.options, includeHeaders); err != nil {
return err
}
h.lastType = h.defaultHandler
return nil
}
@ -631,6 +531,87 @@ func (h *HumanReadablePrinter) PrintTable(obj runtime.Object, options PrintOptio
return table, nil
}
// printRowsForHandlerEntry prints the incremental table output (headers if the current type is
// different from lastType) including all the rows in the object. It returns the current type
// or an error, if any.
func printRowsForHandlerEntry(output io.Writer, handler *handlerEntry, obj runtime.Object, options PrintOptions, includeHeaders bool) error {
if includeHeaders {
var headers []string
for _, column := range handler.columnDefinitions {
if column.Priority != 0 && !options.Wide {
continue
}
headers = append(headers, strings.ToUpper(column.Name))
}
headers = append(headers, formatLabelHeaders(options.ColumnLabels)...)
// LABELS is always the last column.
headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...)
if options.WithNamespace {
headers = append(withNamespacePrefixColumns, headers...)
}
printHeader(headers, output)
}
if !handler.printRows {
// TODO: this code path is deprecated and will be removed when all handlers are row printers
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(output), reflect.ValueOf(options)}
resultValue := handler.printFunc.Call(args)[0]
if resultValue.IsNil() {
return nil
}
return resultValue.Interface().(error)
}
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
results := handler.printFunc.Call(args)
if results[1].IsNil() {
rows := results[0].Interface().([]metav1alpha1.TableRow)
printRows(output, rows, options)
return nil
}
return results[1].Interface().(error)
}
// printRows writes the provided rows to output.
func printRows(output io.Writer, rows []metav1alpha1.TableRow, options PrintOptions) {
for _, row := range rows {
if options.WithNamespace {
if obj := row.Object.Object; obj != nil {
if m, err := meta.Accessor(obj); err == nil {
fmt.Fprint(output, m.GetNamespace())
}
}
fmt.Fprint(output, "\t")
}
for i, cell := range row.Cells {
if i != 0 {
fmt.Fprint(output, "\t")
} else {
// TODO: remove this once we drop the legacy printers
if options.WithKind && len(options.Kind) > 0 {
fmt.Fprintf(output, "%s/%s", options.Kind, cell)
continue
}
}
fmt.Fprint(output, cell)
}
hasLabels := len(options.ColumnLabels) > 0
if obj := row.Object.Object; obj != nil && (hasLabels || options.ShowLabels) {
if m, err := meta.Accessor(obj); err == nil {
for _, value := range labelValues(m.GetLabels(), options) {
output.Write([]byte("\t"))
output.Write([]byte(value))
}
}
}
output.Write([]byte("\n"))
}
}
// legacyPrinterToTable uses the old printFunc with tabbed writer to generate a table.
// TODO: remove when all legacy printers are removed.
func (h *HumanReadablePrinter) legacyPrinterToTable(obj runtime.Object, handler *handlerEntry) (*metav1alpha1.Table, error) {
@ -754,12 +735,9 @@ func formatLabelHeaders(columnLabels []string) []string {
}
// headers for --show-labels=true
func formatShowLabelsHeader(showLabels bool, t reflect.Type) []string {
func formatShowLabelsHeader(showLabels bool) []string {
if showLabels {
// TODO: this is all sorts of hack, fix
if t.String() != "*api.ThirdPartyResource" && t.String() != "*api.ThirdPartyResourceList" {
return []string{"LABELS"}
}
return []string{"LABELS"}
}
return nil
}

View File

@ -1,90 +0,0 @@
/*
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+<none>",
},
}
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)
}
}
}

View File

@ -75,7 +75,6 @@ go_library(
"//pkg/apis/networking:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/apis/rbac:go_default_library",
"//pkg/apis/settings:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/apis/storage/util:go_default_library",
"//pkg/client/clientset_generated/clientset:go_default_library",

View File

@ -30,6 +30,7 @@ import (
batchv2alpha1 "k8s.io/api/batch/v2alpha1"
apiv1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1"
"k8s.io/apimachinery/pkg/labels"
@ -47,7 +48,6 @@ import (
"k8s.io/kubernetes/pkg/apis/networking"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/apis/rbac"
"k8s.io/kubernetes/pkg/apis/settings"
"k8s.io/kubernetes/pkg/apis/storage"
storageutil "k8s.io/kubernetes/pkg/apis/storage/util"
"k8s.io/kubernetes/pkg/controller"
@ -68,8 +68,6 @@ var (
nodeColumns = []string{"NAME", "STATUS", "AGE", "VERSION"}
nodeWideColumns = []string{"EXTERNAL-IP", "OS-IMAGE", "KERNEL-VERSION", "CONTAINER-RUNTIME"}
eventColumns = []string{"LASTSEEN", "FIRSTSEEN", "COUNT", "NAME", "KIND", "SUBOBJECT", "TYPE", "REASON", "SOURCE", "MESSAGE"}
limitRangeColumns = []string{"NAME", "AGE"}
resourceQuotaColumns = []string{"NAME", "AGE"}
namespaceColumns = []string{"NAME", "STATUS", "AGE"}
secretColumns = []string{"NAME", "TYPE", "DATA", "AGE"}
serviceAccountColumns = []string{"NAME", "SECRETS", "AGE"}
@ -77,10 +75,8 @@ var (
persistentVolumeClaimColumns = []string{"NAME", "STATUS", "VOLUME", "CAPACITY", "ACCESSMODES", "STORAGECLASS", "AGE"}
componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"}
thirdPartyResourceColumns = []string{"NAME", "DESCRIPTION", "VERSION(S)"}
roleColumns = []string{"NAME", "AGE"}
roleBindingColumns = []string{"NAME", "AGE"}
roleBindingWideColumns = []string{"ROLE", "USERS", "GROUPS", "SERVICEACCOUNTS"}
clusterRoleColumns = []string{"NAME", "AGE"}
clusterRoleBindingColumns = []string{"NAME", "AGE"}
clusterRoleBindingWideColumns = []string{"ROLE", "USERS", "GROUPS", "SERVICEACCOUNTS"}
storageClassColumns = []string{"NAME", "PROVISIONER"}
@ -214,10 +210,6 @@ func AddHandlers(h printers.PrintHandler) {
h.Handler(nodeColumns, nodeWideColumns, printNodeList)
h.Handler(eventColumns, nil, printEvent)
h.Handler(eventColumns, nil, printEventList)
h.Handler(limitRangeColumns, nil, printLimitRange)
h.Handler(limitRangeColumns, nil, printLimitRangeList)
h.Handler(resourceQuotaColumns, nil, printResourceQuota)
h.Handler(resourceQuotaColumns, nil, printResourceQuotaList)
h.Handler(namespaceColumns, nil, printNamespace)
h.Handler(namespaceColumns, nil, printNamespaceList)
h.Handler(secretColumns, nil, printSecret)
@ -248,23 +240,59 @@ func AddHandlers(h printers.PrintHandler) {
h.Handler(networkPolicyColumns, nil, printExtensionsNetworkPolicyList)
h.Handler(networkPolicyColumns, nil, printNetworkPolicy)
h.Handler(networkPolicyColumns, nil, printNetworkPolicyList)
h.Handler(roleColumns, nil, printRole)
h.Handler(roleColumns, nil, printRoleList)
h.Handler(roleBindingColumns, roleBindingWideColumns, printRoleBinding)
h.Handler(roleBindingColumns, roleBindingWideColumns, printRoleBindingList)
h.Handler(clusterRoleColumns, nil, printClusterRole)
h.Handler(clusterRoleColumns, nil, printClusterRoleList)
h.Handler(clusterRoleBindingColumns, clusterRoleBindingWideColumns, printClusterRoleBinding)
h.Handler(clusterRoleBindingColumns, clusterRoleBindingWideColumns, printClusterRoleBindingList)
h.Handler(certificateSigningRequestColumns, nil, printCertificateSigningRequest)
h.Handler(certificateSigningRequestColumns, nil, printCertificateSigningRequestList)
h.Handler(storageClassColumns, nil, printStorageClass)
h.Handler(storageClassColumns, nil, printStorageClassList)
h.Handler(podPresetColumns, nil, printPodPreset)
h.Handler(podPresetColumns, nil, printPodPresetList)
h.Handler(statusColumns, nil, printStatus)
h.Handler(controllerRevisionColumns, nil, printControllerRevision)
h.Handler(controllerRevisionColumns, nil, printControllerRevisionList)
AddDefaultHandlers(h)
}
// AddDefaultHandlers adds handlers that can work with most Kubernetes objects.
func AddDefaultHandlers(h printers.PrintHandler) {
// types without defined columns
objectMetaColumnDefinitions := []metav1alpha1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
h.DefaultTableHandler(objectMetaColumnDefinitions, printObjectMeta)
}
func printObjectMeta(obj runtime.Object, options printers.PrintOptions) ([]metav1alpha1.TableRow, error) {
if meta.IsListType(obj) {
rows := make([]metav1alpha1.TableRow, 0, 16)
err := meta.EachListItem(obj, func(obj runtime.Object) error {
nestedRows, err := printObjectMeta(obj, options)
if err != nil {
return err
}
rows = append(rows, nestedRows...)
return nil
})
if err != nil {
return nil, err
}
return rows, nil
}
rows := make([]metav1alpha1.TableRow, 0, 1)
m, err := meta.Accessor(obj)
if err != nil {
return nil, err
}
row := metav1alpha1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
row.Cells = append(row.Cells, m.GetName(), translateTimestamp(m.GetCreationTimestamp()))
rows = append(rows, row)
return rows, nil
}
// Pass ports=nil for all ports.
@ -1231,72 +1259,6 @@ func printEventList(list *api.EventList, w io.Writer, options printers.PrintOpti
return nil
}
func printLimitRange(limitRange *api.LimitRange, w io.Writer, options printers.PrintOptions) error {
return printObjectMeta(limitRange.ObjectMeta, w, options, true)
}
// Prints the LimitRangeList in a human-friendly format.
func printLimitRangeList(list *api.LimitRangeList, w io.Writer, options printers.PrintOptions) error {
for i := range list.Items {
if err := printLimitRange(&list.Items[i], w, options); err != nil {
return err
}
}
return nil
}
// printObjectMeta prints the object metadata of a given resource.
func printObjectMeta(meta metav1.ObjectMeta, w io.Writer, options printers.PrintOptions, namespaced bool) error {
name := printers.FormatResourceName(options.Kind, meta.Name, options.WithKind)
if namespaced && options.WithNamespace {
if _, err := fmt.Fprintf(w, "%s\t", meta.Namespace); err != nil {
return err
}
}
if _, err := fmt.Fprintf(
w, "%s\t%s",
name,
translateTimestamp(meta.CreationTimestamp),
); err != nil {
return err
}
if _, err := fmt.Fprint(w, printers.AppendLabels(meta.Labels, options.ColumnLabels)); err != nil {
return err
}
_, err := fmt.Fprint(w, printers.AppendAllLabels(options.ShowLabels, meta.Labels))
return err
}
func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer, options printers.PrintOptions) error {
return printObjectMeta(resourceQuota.ObjectMeta, w, options, true)
}
// Prints the ResourceQuotaList in a human-friendly format.
func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer, options printers.PrintOptions) error {
for i := range list.Items {
if err := printResourceQuota(&list.Items[i], w, options); err != nil {
return err
}
}
return nil
}
func printRole(role *rbac.Role, w io.Writer, options printers.PrintOptions) error {
return printObjectMeta(role.ObjectMeta, w, options, true)
}
// Prints the Role in a human-friendly format.
func printRoleList(list *rbac.RoleList, w io.Writer, options printers.PrintOptions) error {
for i := range list.Items {
if err := printRole(&list.Items[i], w, options); err != nil {
return err
}
}
return nil
}
func printRoleBinding(roleBinding *rbac.RoleBinding, w io.Writer, options printers.PrintOptions) error {
meta := roleBinding.ObjectMeta
name := printers.FormatResourceName(options.Kind, meta.Name, options.WithKind)
@ -1345,23 +1307,6 @@ func printRoleBindingList(list *rbac.RoleBindingList, w io.Writer, options print
return nil
}
func printClusterRole(clusterRole *rbac.ClusterRole, w io.Writer, options printers.PrintOptions) error {
if options.WithNamespace {
return fmt.Errorf("clusterRole is not namespaced")
}
return printObjectMeta(clusterRole.ObjectMeta, w, options, false)
}
// Prints the ClusterRole in a human-friendly format.
func printClusterRoleList(list *rbac.ClusterRoleList, w io.Writer, options printers.PrintOptions) error {
for i := range list.Items {
if err := printClusterRole(&list.Items[i], w, options); err != nil {
return err
}
}
return nil
}
func printClusterRoleBinding(clusterRoleBinding *rbac.ClusterRoleBinding, w io.Writer, options printers.PrintOptions) error {
meta := clusterRoleBinding.ObjectMeta
name := printers.FormatResourceName(options.Kind, meta.Name, options.WithKind)
@ -1864,19 +1809,6 @@ func printStorageClassList(scList *storage.StorageClassList, w io.Writer, option
return nil
}
func printPodPreset(podPreset *settings.PodPreset, w io.Writer, options printers.PrintOptions) error {
return printObjectMeta(podPreset.ObjectMeta, w, options, false)
}
func printPodPresetList(list *settings.PodPresetList, w io.Writer, options printers.PrintOptions) error {
for i := range list.Items {
if err := printPodPreset(&list.Items[i], w, options); err != nil {
return err
}
}
return nil
}
func printStatus(status *metav1.Status, w io.Writer, options printers.PrintOptions) error {
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\n", status.Status, status.Reason, status.Message); err != nil {
return err

View File

@ -22,6 +22,7 @@ import (
"fmt"
"io"
"reflect"
"regexp"
"strconv"
"strings"
"testing"
@ -103,6 +104,112 @@ func TestPrintDefault(t *testing.T) {
}
}
func TestPrintUnstructuredObject(t *testing.T) {
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",
"labels": map[string]interface{}{"test": "other"},
},
/*"items": []interface{}{
map[string]interface{}{
"itemBool": true,
"itemInt": 42,
},
},*/
"url": "http://localhost",
"status": "ok",
},
}
tests := []struct {
expected string
options printers.PrintOptions
object runtime.Object
}{
{
expected: "NAME\\s+AGE\nMyName\\s+\\d+",
object: obj,
},
{
options: printers.PrintOptions{
WithNamespace: true,
},
expected: "NAMESPACE\\s+NAME\\s+AGE\nMyNamespace\\s+MyName\\s+\\d+",
object: obj,
},
{
options: printers.PrintOptions{
ShowLabels: true,
WithNamespace: true,
},
expected: "NAMESPACE\\s+NAME\\s+AGE\\s+LABELS\nMyNamespace\\s+MyName\\s+\\d+\\w+\\s+test\\=other",
object: obj,
},
{
expected: "NAME\\s+AGE\nMyName\\s+\\d+\\w+\nMyName2\\s+\\d+",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Test",
"dummy1": "present",
"dummy2": "present",
"items": []interface{}{
map[string]interface{}{
"metadata": map[string]interface{}{
"name": "MyName",
"namespace": "MyNamespace",
"creationTimestamp": "2017-04-01T00:00:00Z",
"resourceVersion": 123,
"uid": "00000000-0000-0000-0000-000000000001",
"dummy3": "present",
"labels": map[string]interface{}{"test": "other"},
},
},
map[string]interface{}{
"metadata": map[string]interface{}{
"name": "MyName2",
"namespace": "MyNamespace",
"creationTimestamp": "2017-04-01T00:00:00Z",
"resourceVersion": 123,
"uid": "00000000-0000-0000-0000-000000000001",
"dummy3": "present",
"labels": "badlabel",
},
},
},
"url": "http://localhost",
"status": "ok",
},
},
},
}
out := bytes.NewBuffer([]byte{})
for _, test := range tests {
out.Reset()
printer := printers.NewHumanReadablePrinter(nil, nil, test.options).With(AddDefaultHandlers)
printer.PrintObj(test.object, out)
matches, err := regexp.MatchString(test.expected, out.String())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !matches {
t.Errorf("wanted:\n%s\ngot:\n%s", test.expected, out)
}
}
}
type TestPrintType struct {
Data string
}

View File

@ -67,6 +67,9 @@ func GetItemsPtr(list runtime.Object) (interface{}, error) {
// EachListItem invokes fn on each runtime.Object in the list. Any error immediately terminates
// the loop.
func EachListItem(obj runtime.Object, fn func(runtime.Object) error) error {
if unstructured, ok := obj.(runtime.Unstructured); ok {
return unstructured.EachListItem(fn)
}
// TODO: Change to an interface call?
itemsPtr, err := GetItemsPtr(obj)
if err != nil {

View File

@ -69,6 +69,39 @@ func (obj *Unstructured) IsList() bool {
}
func (obj *UnstructuredList) IsList() bool { return true }
func (obj *Unstructured) EachListItem(fn func(runtime.Object) error) error {
if obj.Object == nil {
return fmt.Errorf("content is not a list")
}
field, ok := obj.Object["items"]
if !ok {
return fmt.Errorf("content is not a list")
}
items, ok := field.([]interface{})
if !ok {
return nil
}
for _, item := range items {
child, ok := item.(map[string]interface{})
if !ok {
return fmt.Errorf("items member is not an object")
}
if err := fn(&Unstructured{Object: child}); err != nil {
return err
}
}
return nil
}
func (obj *UnstructuredList) EachListItem(fn func(runtime.Object) error) error {
for i := range obj.Items {
if err := fn(&obj.Items[i]); err != nil {
return err
}
}
return nil
}
func (obj *Unstructured) UnstructuredContent() map[string]interface{} {
if obj.Object == nil {
obj.Object = make(map[string]interface{})

View File

@ -242,10 +242,14 @@ type Unstructured interface {
// IsUnstructuredObject is a marker interface to allow objects that can be serialized but not introspected
// to bypass conversion.
IsUnstructuredObject()
// IsList returns true if this type is a list or matches the list convention - has an array called "items".
IsList() bool
// UnstructuredContent returns a non-nil, mutable map of the contents of this object. Values may be
// []interface{}, map[string]interface{}, or any primitive type. Contents are typically serialized to
// and from JSON.
UnstructuredContent() map[string]interface{}
// IsList returns true if this type is a list or matches the list convention - has an array called "items".
IsList() bool
// EachListItem should pass a single item out of the list as an Object to the provided function. Any
// error should terminate the iteration. If IsList() returns false, this method should return an error
// instead of calling the provided function.
EachListItem(func(Object) error) error
}