diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 6fec9f56fa4..c4e2f3a6e64 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -192,7 +192,7 @@ func Example_printReplicationControllerWithNamespace() { }, } mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, ctrl, os.Stdout) + err := f.PrintObject(cmd, mapper, ctrl, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -247,7 +247,7 @@ func Example_printMultiContainersReplicationControllerWithWide() { }, } mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, ctrl, os.Stdout) + err := f.PrintObject(cmd, mapper, ctrl, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -301,7 +301,7 @@ func Example_printReplicationController() { }, } mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, ctrl, os.Stdout) + err := f.PrintObject(cmd, mapper, ctrl, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -344,7 +344,7 @@ func Example_printPodWithWideFormat() { }, } mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, pod, os.Stdout) + err := f.PrintObject(cmd, mapper, pod, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -390,7 +390,7 @@ func Example_printPodWithShowLabels() { }, } mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, pod, os.Stdout) + err := f.PrintObject(cmd, mapper, pod, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -514,7 +514,7 @@ func Example_printPodHideTerminated() { } for _, pod := range filteredPodList { mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, pod, os.Stdout) + err := f.PrintObject(cmd, mapper, pod, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -542,7 +542,7 @@ func Example_printPodShowAll() { cmd := NewCmdRun(f, os.Stdin, os.Stdout, os.Stderr) podList := newAllPhasePodList() mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, podList, os.Stdout) + err := f.PrintObject(cmd, mapper, podList, printers.GetNewTabWriter(os.Stdout)) if err != nil { fmt.Printf("Unexpected error: %v", err) } @@ -616,9 +616,10 @@ func Example_printServiceWithNamespacesAndLabels() { } ld := strings.NewLineDelimiter(os.Stdout, "|") defer ld.Flush() - + out := printers.GetNewTabWriter(ld) + defer out.Flush() mapper, _ := f.Object() - err := f.PrintObject(cmd, mapper, svc, ld) + err := f.PrintObject(cmd, mapper, svc, out) if err != nil { fmt.Printf("Unexpected error: %v", err) } diff --git a/pkg/printers/BUILD b/pkg/printers/BUILD index 8e6e4b4da2b..c888148e23b 100644 --- a/pkg/printers/BUILD +++ b/pkg/printers/BUILD @@ -28,12 +28,14 @@ go_library( "//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", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/client-go/util/jsonpath:go_default_library", ], ) @@ -63,6 +65,7 @@ filegroup( srcs = [ ":package-srcs", "//pkg/printers/internalversion:all-srcs", + "//pkg/printers/storage:all-srcs", ], tags = ["automanaged"], ) diff --git a/pkg/printers/humanreadable.go b/pkg/printers/humanreadable.go index 8517ef7ce3d..83fda3a67a8 100644 --- a/pkg/printers/humanreadable.go +++ b/pkg/printers/humanreadable.go @@ -26,22 +26,33 @@ import ( "text/tabwriter" "github.com/fatih/camelcase" - "github.com/golang/glog" "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" "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 { + PrintTable(obj runtime.Object, options PrintOptions) (*metav1alpha1.Table, error) +} + +type PrintHandler interface { + Handler(columns, columnsWithWide []string, printFunc interface{}) error + TableHandler(columns []metav1alpha1.TableColumnDefinition, printFunc interface{}) error +} + var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too. type handlerEntry struct { - columns []string - columnsWithWide []string - printFunc reflect.Value - args []reflect.Value + columnDefinitions []metav1alpha1.TableColumnDefinition + printRows bool + printFunc reflect.Value + args []reflect.Value } // HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide @@ -57,6 +68,8 @@ type HumanReadablePrinter struct { decoder runtime.Decoder } +var _ PrintHandler = &HumanReadablePrinter{} + // NewHumanReadablePrinter creates a HumanReadablePrinter. // If encoder and decoder are provided, an attempt to convert unstructured types to internal types is made. func NewHumanReadablePrinter(encoder runtime.Encoder, decoder runtime.Decoder, options PrintOptions) *HumanReadablePrinter { @@ -69,6 +82,20 @@ func NewHumanReadablePrinter(encoder runtime.Encoder, decoder runtime.Decoder, o return printer } +// NewTablePrinter creates a HumanReadablePrinter suitable for calling PrintTable(). +func NewTablePrinter() *HumanReadablePrinter { + return &HumanReadablePrinter{ + handlerMap: make(map[reflect.Type]*handlerEntry), + } +} + +func (a *HumanReadablePrinter) With(fns ...func(PrintHandler)) *HumanReadablePrinter { + for _, fn := range fns { + fn(a) + } + return a +} + // GetResourceKind returns the type currently set for a resource func (h *HumanReadablePrinter) GetResourceKind() string { return h.options.Kind @@ -92,29 +119,100 @@ func (h *HumanReadablePrinter) EnsurePrintHeaders() { } // Handler adds a print handler with a given set of columns to HumanReadablePrinter instance. -// See validatePrintHandlerFunc for required method signature. +// See ValidatePrintHandlerFunc for required method signature. func (h *HumanReadablePrinter) Handler(columns, columnsWithWide []string, printFunc interface{}) error { + var columnDefinitions []metav1alpha1.TableColumnDefinition + for _, column := range columns { + columnDefinitions = append(columnDefinitions, metav1alpha1.TableColumnDefinition{ + Name: column, + Type: "string", + }) + } + for _, column := range columnsWithWide { + columnDefinitions = append(columnDefinitions, metav1alpha1.TableColumnDefinition{ + Name: column, + Type: "string", + Priority: 1, + }) + } + printFuncValue := reflect.ValueOf(printFunc) - if err := h.validatePrintHandlerFunc(printFuncValue); err != nil { - glog.Errorf("Unable to add print handler: %v", err) + if err := ValidatePrintHandlerFunc(printFuncValue); err != nil { + utilruntime.HandleError(fmt.Errorf("unable to register print function: %v", err)) return err } + entry := &handlerEntry{ + columnDefinitions: columnDefinitions, + printFunc: printFuncValue, + } + objType := printFuncValue.Type().In(0) - h.handlerMap[objType] = &handlerEntry{ - columns: columns, - columnsWithWide: columnsWithWide, - printFunc: printFuncValue, + if _, ok := h.handlerMap[objType]; ok { + err := fmt.Errorf("registered duplicate printer for %v", objType) + utilruntime.HandleError(err) + return err + } + h.handlerMap[objType] = entry + return nil +} + +// TableHandler adds a print handler with a given set of columns to HumanReadablePrinter instance. +// See ValidateRowPrintHandlerFunc for required method signature. +func (h *HumanReadablePrinter) TableHandler(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, + } + + objType := printFuncValue.Type().In(0) + if _, ok := h.handlerMap[objType]; ok { + err := fmt.Errorf("registered duplicate printer for %v", objType) + utilruntime.HandleError(err) + return err + } + h.handlerMap[objType] = 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: +// func printFunc(object ObjectType, options PrintOptions) ([]metav1alpha1.TableRow, error) +// where ObjectType is the type of the object that will be printed, and the first +// return value is an array of rows, with each row containing a number of cells that +// match the number of coulmns defined for that printer function. +func ValidateRowPrintHandlerFunc(printFunc reflect.Value) error { + if printFunc.Kind() != reflect.Func { + return fmt.Errorf("invalid print handler. %#v is not a function", printFunc) + } + funcType := printFunc.Type() + if funcType.NumIn() != 2 || funcType.NumOut() != 2 { + return fmt.Errorf("invalid print handler." + + "Must accept 2 parameters and return 2 value.") + } + if funcType.In(1) != reflect.TypeOf((*PrintOptions)(nil)).Elem() || + funcType.Out(0) != reflect.TypeOf((*[]metav1alpha1.TableRow)(nil)).Elem() || + funcType.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { + return fmt.Errorf("invalid print handler. The expected signature is: "+ + "func handler(obj %v, options PrintOptions) ([]metav1alpha1.TableRow, error)", funcType.In(0)) } return nil } -// validatePrintHandlerFunc validates print handler signature. +// ValidatePrintHandlerFunc validates print handler signature. // printFunc is the function that will be called to print an object. // It must be of the following type: // func printFunc(object ObjectType, w io.Writer, options PrintOptions) error // where ObjectType is the type of the object that will be printed. -func (h *HumanReadablePrinter) validatePrintHandlerFunc(printFunc reflect.Value) error { +// DEPRECATED: will be replaced with ValidateRowPrintHandlerFunc +func ValidatePrintHandlerFunc(printFunc reflect.Value) error { if printFunc.Kind() != reflect.Func { return fmt.Errorf("invalid print handler. %#v is not a function", printFunc) } @@ -167,12 +265,18 @@ func (h *HumanReadablePrinter) printHeader(columnNames []string, w io.Writer) er // PrintObj prints the obj in a human-friendly format according to the type of the obj. func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error { // if output is a tabwriter (when it's called by kubectl get), we use it; create a new tabwriter otherwise - w, found := output.(*tabwriter.Writer) - if !found { - w = GetNewTabWriter(output) + if w, found := output.(*tabwriter.Writer); found { defer w.Flush() } + // display tables following the rules of options + if table, ok := obj.(*metav1alpha1.Table); ok { + if err := DecorateTable(table, h.options); err != nil { + return err + } + return PrintTable(table, output, h.options) + } + // check if the object is unstructured. If so, let's attempt to convert it to a type we can understand before // trying to print, since the printers are keyed by type. This is extremely expensive. if h.encoder != nil && h.decoder != nil { @@ -182,9 +286,12 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er t := reflect.TypeOf(obj) if handler := h.handlerMap[t]; handler != nil { if !h.options.NoHeaders && t != h.lastType { - headers := handler.columns - if h.options.Wide { - headers = append(headers, handler.columnsWithWide...) + 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. @@ -192,10 +299,58 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er if h.options.WithNamespace { headers = append(withNamespacePrefixColumns, headers...) } - h.printHeader(headers, w) + h.printHeader(headers, output) h.lastType = t } - args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w), reflect.ValueOf(h.options)} + + 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 @@ -207,7 +362,7 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er // 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) + return fmt.Errorf("error: unknown type %T, expected unstructured in %#v", obj, h.handlerMap) } content := unstructured.UnstructuredContent() @@ -255,12 +410,12 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er if h.options.WithNamespace { headers = append(withNamespacePrefixColumns, headers...) } - h.printHeader(headers, w) + 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, w, discoveredFieldNames, h.options); err != nil { + if err := printUnstructured(unstructured, output, discoveredFieldNames, h.options); err != nil { return err } return nil @@ -270,6 +425,250 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er return fmt.Errorf("error: unknown type %#v", obj) } +func hasCondition(conditions []metav1alpha1.TableRowCondition, t metav1alpha1.RowConditionType) bool { + for _, condition := range conditions { + if condition.Type == t { + return condition.Status == metav1alpha1.ConditionTrue + } + } + return false +} + +// PrintTable prints a table to the provided output respecting the filtering rules for options +// for wide columns and filetred rows. It filters out rows that are Completed. You should call +// DecorateTable if you receive a table from a remote server before calling PrintTable. +func PrintTable(table *metav1alpha1.Table, output io.Writer, options PrintOptions) error { + if !options.NoHeaders { + first := true + for _, column := range table.ColumnDefinitions { + if !options.Wide && column.Priority != 0 { + continue + } + if first { + first = false + } else { + fmt.Fprint(output, "\t") + } + fmt.Fprint(output, strings.ToUpper(column.Name)) + } + fmt.Fprintln(output) + } + for _, row := range table.Rows { + if !options.ShowAll && hasCondition(row.Conditions, metav1alpha1.RowCompleted) { + continue + } + first := true + for i, cell := range row.Cells { + column := table.ColumnDefinitions[i] + if !options.Wide && column.Priority != 0 { + continue + } + if first { + first = false + } else { + fmt.Fprint(output, "\t") + } + if cell != nil { + fmt.Fprint(output, cell) + } + } + fmt.Fprintln(output) + } + return nil +} + +// DecorateTable takes a table and attempts to add label columns and the +// namespace column. It will fill empty columns with nil (if the object +// does not expose metadata). It returns an error if the table cannot +// be decorated. +func DecorateTable(table *metav1alpha1.Table, options PrintOptions) error { + width := len(table.ColumnDefinitions) + len(options.ColumnLabels) + if options.WithNamespace { + width++ + } + if options.ShowLabels { + width++ + } + + columns := table.ColumnDefinitions + + nameColumn := -1 + if options.WithKind && len(options.Kind) > 0 { + for i := range columns { + if columns[i].Format == "name" && columns[i].Type == "string" { + nameColumn = i + fmt.Printf("found name column: %d\n", i) + break + } + } + } + + if width != len(table.ColumnDefinitions) { + columns = make([]metav1alpha1.TableColumnDefinition, 0, width) + if options.WithNamespace { + columns = append(columns, metav1alpha1.TableColumnDefinition{ + Name: "Namespace", + Type: "string", + }) + } + columns = append(columns, table.ColumnDefinitions...) + for _, label := range formatLabelHeaders(options.ColumnLabels) { + columns = append(columns, metav1alpha1.TableColumnDefinition{ + Name: label, + Type: "string", + }) + } + if options.ShowLabels { + columns = append(columns, metav1alpha1.TableColumnDefinition{ + Name: "Labels", + Type: "string", + }) + } + } + + rows := table.Rows + + includeLabels := len(options.ColumnLabels) > 0 || options.ShowLabels + if includeLabels || options.WithNamespace || nameColumn != -1 { + for i := range rows { + row := rows[i] + + if nameColumn != -1 { + row.Cells[nameColumn] = fmt.Sprintf("%s/%s", options.Kind, row.Cells[nameColumn]) + } + + var m metav1.Object + if obj := row.Object.Object; obj != nil { + if acc, err := meta.Accessor(obj); err == nil { + m = acc + } + } + // if we can't get an accessor, fill out the appropriate columns with empty spaces + if m == nil { + if options.WithNamespace { + r := make([]interface{}, 1, width) + row.Cells = append(r, row.Cells...) + } + for j := 0; j < width-len(row.Cells); j++ { + row.Cells = append(row.Cells, nil) + } + rows[i] = row + continue + } + + if options.WithNamespace { + r := make([]interface{}, 1, width) + r[0] = m.GetNamespace() + row.Cells = append(r, row.Cells...) + } + if includeLabels { + row.Cells = appendLabelCells(row.Cells, m.GetLabels(), options) + } + rows[i] = row + } + } + + table.ColumnDefinitions = columns + table.Rows = rows + return nil +} + +// PrintTable returns a table for the provided object, using the printer registered for that type. It returns +// a table that includes all of the information requested by options, but will not remove rows or columns. The +// caller is responsible for applying rules related to filtering rows or columns. +func (h *HumanReadablePrinter) PrintTable(obj runtime.Object, options PrintOptions) (*metav1alpha1.Table, error) { + t := reflect.TypeOf(obj) + handler, ok := h.handlerMap[t] + if !ok { + return nil, fmt.Errorf("no table handler registered for this type %v", t) + } + if !handler.printRows { + return h.legacyPrinterToTable(obj, handler) + } + + args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)} + results := handler.printFunc.Call(args) + if !results[1].IsNil() { + return nil, results[1].Interface().(error) + } + + columns := handler.columnDefinitions + if !options.Wide { + columns = make([]metav1alpha1.TableColumnDefinition, 0, len(handler.columnDefinitions)) + for i := range handler.columnDefinitions { + if handler.columnDefinitions[i].Priority != 0 { + continue + } + columns = append(columns, handler.columnDefinitions[i]) + } + } + table := &metav1alpha1.Table{ + ListMeta: metav1.ListMeta{ + ResourceVersion: "", + }, + ColumnDefinitions: columns, + Rows: results[0].Interface().([]metav1alpha1.TableRow), + } + if err := DecorateTable(table, options); err != nil { + return nil, err + } + return table, nil +} + +// 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) { + printFunc := handler.printFunc + table := &metav1alpha1.Table{ + ColumnDefinitions: handler.columnDefinitions, + } + + options := PrintOptions{ + NoHeaders: true, + Wide: true, + } + buf := &bytes.Buffer{} + args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(buf), reflect.ValueOf(options)} + + if meta.IsListType(obj) { + // TODO: this uses more memory than it has to, as we refactor printers we should remove the need + // for this. + args[0] = reflect.ValueOf(obj) + resultValue := printFunc.Call(args)[0] + if !resultValue.IsNil() { + return nil, resultValue.Interface().(error) + } + data := buf.Bytes() + i := 0 + items, err := meta.ExtractList(obj) + if err != nil { + return nil, err + } + for len(data) > 0 { + cells, remainder := tabbedLineToCells(data, len(table.ColumnDefinitions)) + table.Rows = append(table.Rows, metav1alpha1.TableRow{ + Cells: cells, + Object: runtime.RawExtension{Object: items[i]}, + }) + data = remainder + i++ + } + } else { + args[0] = reflect.ValueOf(obj) + resultValue := printFunc.Call(args)[0] + if !resultValue.IsNil() { + return nil, resultValue.Interface().(error) + } + data := buf.Bytes() + cells, _ := tabbedLineToCells(data, len(table.ColumnDefinitions)) + table.Rows = append(table.Rows, metav1alpha1.TableRow{ + Cells: cells, + Object: runtime.RawExtension{Object: obj}, + }) + } + return table, nil +} + // 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, additionalFields []string, options PrintOptions) error { metadata, err := meta.Accessor(unstructured) @@ -349,6 +748,30 @@ func formatShowLabelsHeader(showLabels bool, t reflect.Type) []string { return nil } +// labelValues returns a slice of value columns matching the requested print options. +func labelValues(itemLabels map[string]string, opts PrintOptions) []string { + var values []string + for _, key := range opts.ColumnLabels { + values = append(values, itemLabels[key]) + } + if opts.ShowLabels { + values = append(values, labels.FormatLabels(itemLabels)) + } + return values +} + +// appendLabelCells returns a slice of value columns matching the requested print options. +// Intended for use with tables. +func appendLabelCells(values []interface{}, itemLabels map[string]string, opts PrintOptions) []interface{} { + for _, key := range opts.ColumnLabels { + values = append(values, itemLabels[key]) + } + if opts.ShowLabels { + values = append(values, labels.FormatLabels(itemLabels)) + } + return values +} + // FormatResourceName receives a resource kind, name, and boolean specifying // whether or not to update the current name to "kind/name" func FormatResourceName(kind, name string, withKind bool) string { @@ -402,3 +825,27 @@ func decodeUnknownObject(obj runtime.Object, encoder runtime.Encoder, decoder ru return obj, err } + +func tabbedLineToCells(data []byte, expected int) ([]interface{}, []byte) { + var remainder []byte + max := bytes.Index(data, []byte("\n")) + if max != -1 { + remainder = data[max+1:] + data = data[:max] + } + cells := make([]interface{}, expected) + for i := 0; i < expected; i++ { + next := bytes.Index(data, []byte("\t")) + if next == -1 { + cells[i] = string(data) + // fill the remainder with empty strings, this indicates a printer bug + for j := i + 1; j < expected; j++ { + cells[j] = "" + } + break + } + cells[i] = string(data[:next]) + data = data[next+1:] + } + return cells, remainder +} diff --git a/pkg/printers/internalversion/BUILD b/pkg/printers/internalversion/BUILD index 8388489de24..784160b5287 100644 --- a/pkg/printers/internalversion/BUILD +++ b/pkg/printers/internalversion/BUILD @@ -40,6 +40,7 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml:go_default_library", @@ -65,6 +66,7 @@ go_library( "//pkg/api/helper/qos:go_default_library", "//pkg/api/ref:go_default_library", "//pkg/api/resource:go_default_library", + "//pkg/api/v1:go_default_library", "//pkg/apis/apps:go_default_library", "//pkg/apis/autoscaling:go_default_library", "//pkg/apis/batch:go_default_library", @@ -94,8 +96,10 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library", diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index fdc141cdb6f..24498091c6c 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -27,12 +27,15 @@ import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/kubernetes/federation/apis/federation" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/events" "k8s.io/kubernetes/pkg/api/helper" + apiv1 "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/batch" @@ -53,8 +56,6 @@ const loadBalancerWidth = 16 // NOTE: When adding a new resource type here, please update the list // pkg/kubectl/cmd/get.go to reflect the new resource type. var ( - podColumns = []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"} - podWideColumns = []string{"IP", "NODE"} podTemplateColumns = []string{"TEMPLATE", "CONTAINER(S)", "IMAGE(S)", "PODLABELS"} podDisruptionBudgetColumns = []string{"NAME", "MIN-AVAILABLE", "MAX-UNAVAILABLE", "ALLOWED-DISRUPTIONS", "AGE"} replicationControllerColumns = []string{"NAME", "DESIRED", "CURRENT", "READY", "AGE"} @@ -105,27 +106,21 @@ var ( podPresetColumns = []string{"NAME", "AGE"} ) -func printPod(pod *api.Pod, w io.Writer, options printers.PrintOptions) error { - if err := printPodBase(pod, w, options); err != nil { - return err - } - - return nil -} - -func printPodList(podList *api.PodList, w io.Writer, options printers.PrintOptions) error { - for _, pod := range podList.Items { - if err := printPodBase(&pod, w, options); err != nil { - return err - } - } - return nil -} - // AddHandlers adds print handlers for default Kubernetes types dealing with internal versions. -func AddHandlers(h *printers.HumanReadablePrinter) { - h.Handler(podColumns, podWideColumns, printPodList) - h.Handler(podColumns, podWideColumns, printPod) +// TODO: handle errors from Handler +func AddHandlers(h printers.PrintHandler) { + podColumnDefinitions := []metav1alpha1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Ready", Type: "string", Description: "The aggregate readiness state of this pod for accepting traffic."}, + {Name: "Status", Type: "string", Description: "The aggregate status of the containers in this pod."}, + {Name: "Restarts", Type: "integer", Description: "The number of times the containers in this pod have been restarted."}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + {Name: "IP", Type: "string", Priority: 1, Description: apiv1.PodStatus{}.SwaggerDoc()["podIP"]}, + {Name: "Node", Type: "string", Priority: 1, Description: apiv1.PodSpec{}.SwaggerDoc()["nodeName"]}, + } + h.TableHandler(podColumnDefinitions, printPodList) + h.TableHandler(podColumnDefinitions, printPod) + h.Handler(podTemplateColumns, nil, printPodTemplate) h.Handler(podTemplateColumns, nil, printPodTemplateList) h.Handler(podDisruptionBudgetColumns, nil, printPodDisruptionBudget) @@ -247,10 +242,24 @@ func translateTimestamp(timestamp metav1.Time) string { return printers.ShortHumanDuration(time.Now().Sub(timestamp.Time)) } -func printPodBase(pod *api.Pod, w io.Writer, options printers.PrintOptions) error { - name := printers.FormatResourceName(options.Kind, pod.Name, options.WithKind) - namespace := pod.Namespace +var ( + podSuccessConditions = []metav1alpha1.TableRowCondition{{Type: metav1alpha1.RowCompleted, Status: metav1alpha1.ConditionTrue, Reason: string(api.PodSucceeded), Message: "The pod has completed successfully."}} + podFailedConditions = []metav1alpha1.TableRowCondition{{Type: metav1alpha1.RowCompleted, Status: metav1alpha1.ConditionTrue, Reason: string(api.PodFailed), Message: "The pod failed."}} +) +func printPodList(podList *api.PodList, options printers.PrintOptions) ([]metav1alpha1.TableRow, error) { + rows := make([]metav1alpha1.TableRow, 0, len(podList.Items)) + for i := range podList.Items { + r, err := printPod(&podList.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + +func printPod(pod *api.Pod, options printers.PrintOptions) ([]metav1alpha1.TableRow, error) { restarts := 0 totalContainers := len(pod.Spec.Containers) readyContainers := 0 @@ -260,6 +269,17 @@ func printPodBase(pod *api.Pod, w io.Writer, options printers.PrintOptions) erro reason = pod.Status.Reason } + row := metav1alpha1.TableRow{ + Object: runtime.RawExtension{Object: pod}, + } + + switch pod.Status.Phase { + case api.PodSucceeded: + row.Conditions = podSuccessConditions + case api.PodFailed: + row.Conditions = podFailedConditions + } + initializing := false for i := range pod.Status.InitContainerStatuses { container := pod.Status.InitContainerStatuses[i] @@ -316,21 +336,7 @@ func printPodBase(pod *api.Pod, w io.Writer, options printers.PrintOptions) erro reason = "Terminating" } - if options.WithNamespace { - if _, err := fmt.Fprintf(w, "%s\t", namespace); err != nil { - return err - } - } - if _, err := fmt.Fprintf(w, "%s\t%d/%d\t%s\t%d\t%s", - name, - readyContainers, - totalContainers, - reason, - restarts, - translateTimestamp(pod.CreationTimestamp), - ); err != nil { - return err - } + row.Cells = append(row.Cells, pod.Name, fmt.Sprintf("%d/%d", readyContainers, totalContainers), reason, restarts, translateTimestamp(pod.CreationTimestamp)) if options.Wide { nodeName := pod.Spec.NodeName @@ -341,22 +347,10 @@ func printPodBase(pod *api.Pod, w io.Writer, options printers.PrintOptions) erro if nodeName == "" { nodeName = "" } - if _, err := fmt.Fprintf(w, "\t%s\t%s", - podIP, - nodeName, - ); err != nil { - return err - } + row.Cells = append(row.Cells, podIP, nodeName) } - if _, err := fmt.Fprint(w, printers.AppendLabels(pod.Labels, options.ColumnLabels)); err != nil { - return err - } - if _, err := fmt.Fprint(w, printers.AppendAllLabels(options.ShowLabels, pod.Labels)); err != nil { - return err - } - - return nil + return []metav1alpha1.TableRow{row}, nil } func printPodTemplate(pod *api.PodTemplate, w io.Writer, options printers.PrintOptions) error { diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index ff940fb278e..607f3c87747 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "reflect" + "strconv" "strings" "testing" "time" @@ -31,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" yamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" @@ -267,7 +269,7 @@ func TestCustomTypePrinting(t *testing.T) { if err != nil { t.Fatalf("An error occurred printing the custom type: %#v", err) } - expectedOutput := "Data\ntest object" + expectedOutput := "DATA\ntest object" if buffer.String() != expectedOutput { t.Errorf("The data was not printed as expected. Expected:\n%s\nGot:\n%s", expectedOutput, buffer.String()) } @@ -285,7 +287,7 @@ func TestCustomTypePrintingWithKind(t *testing.T) { if err != nil { t.Fatalf("An error occurred printing the custom type: %#v", err) } - expectedOutput := "Data\ntest/test object" + expectedOutput := "DATA\ntest/test object" if buffer.String() != expectedOutput { t.Errorf("The data was not printed as expected. Expected:\n%s\nGot:\n%s", expectedOutput, buffer.String()) } @@ -1252,7 +1254,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { }, } - for _, test := range table { + for i, test := range table { if test.isNamespaced { // Expect output to include namespace when requested. printer := printers.NewHumanReadablePrinter(nil, nil, printers.PrintOptions{ @@ -1266,7 +1268,7 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { } matched := contains(strings.Fields(buffer.String()), fmt.Sprintf("%s", namespaceName)) if !matched { - t.Errorf("Expect printing object to contain namespace: %#v", test.obj) + t.Errorf("%d: Expect printing object to contain namespace: %#v", i, test.obj) } } else { // Expect error when trying to get all namespaces for un-namespaced object. @@ -1282,10 +1284,96 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) { } } +func TestPrintPodTable(t *testing.T) { + runningPod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "test1", Labels: map[string]string{"a": "1", "b": "2"}}, + Spec: api.PodSpec{Containers: make([]api.Container, 2)}, + Status: api.PodStatus{ + Phase: "Running", + ContainerStatuses: []api.ContainerStatus{ + {Ready: true, RestartCount: 3, State: api.ContainerState{Running: &api.ContainerStateRunning{}}}, + {RestartCount: 3}, + }, + }, + } + failedPod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "test2", Labels: map[string]string{"b": "2"}}, + Spec: api.PodSpec{Containers: make([]api.Container, 2)}, + Status: api.PodStatus{ + Phase: "Failed", + ContainerStatuses: []api.ContainerStatus{ + {Ready: true, RestartCount: 3, State: api.ContainerState{Running: &api.ContainerStateRunning{}}}, + {RestartCount: 3}, + }, + }, + } + tests := []struct { + obj runtime.Object + opts printers.PrintOptions + expect string + ignoreLegacy bool + }{ + { + obj: runningPod, opts: printers.PrintOptions{}, + expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\ntest1\t1/2\tRunning\t6\t\n", + }, + { + obj: runningPod, opts: printers.PrintOptions{WithKind: true, Kind: "pods"}, + expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\npods/test1\t1/2\tRunning\t6\t\n", + }, + { + obj: runningPod, opts: printers.PrintOptions{ShowLabels: true}, + expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\tLABELS\ntest1\t1/2\tRunning\t6\t\ta=1,b=2\n", + }, + { + obj: &api.PodList{Items: []api.Pod{*runningPod, *failedPod}}, opts: printers.PrintOptions{ShowAll: true, ColumnLabels: []string{"a"}}, + expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\tA\ntest1\t1/2\tRunning\t6\t\t1\ntest2\t1/2\tFailed\t6\t\t\n", + }, + { + obj: runningPod, opts: printers.PrintOptions{NoHeaders: true}, + expect: "test1\t1/2\tRunning\t6\t\n", + }, + { + obj: failedPod, opts: printers.PrintOptions{}, + expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\n", + ignoreLegacy: true, // filtering is not done by the printer in the legacy path + }, + { + obj: failedPod, opts: printers.PrintOptions{ShowAll: true}, + expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\ntest2\t1/2\tFailed\t6\t\n", + }, + } + + for i, test := range tests { + table, err := printers.NewTablePrinter().With(AddHandlers).PrintTable(test.obj, printers.PrintOptions{}) + if err != nil { + t.Fatal(err) + } + buf := &bytes.Buffer{} + p := printers.NewHumanReadablePrinter(nil, nil, test.opts).With(AddHandlers) + if err := p.PrintObj(table, buf); err != nil { + t.Fatal(err) + } + if test.expect != buf.String() { + t.Errorf("%d mismatch:\n%s\n%s", i, strconv.Quote(test.expect), strconv.Quote(buf.String())) + } + if test.ignoreLegacy { + continue + } + + buf.Reset() + if err := p.PrintObj(test.obj, buf); err != nil { + t.Fatal(err) + } + if test.expect != buf.String() { + t.Errorf("%d legacy mismatch:\n%s\n%s", i, strconv.Quote(test.expect), strconv.Quote(buf.String())) + } + } +} func TestPrintPod(t *testing.T) { tests := []struct { pod api.Pod - expect string + expect []metav1alpha1.TableRow }{ { // Test name, num of containers, restarts, container ready status @@ -1300,7 +1388,7 @@ func TestPrintPod(t *testing.T) { }, }, }, - "test1\t1/2\tpodPhase\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test1", "1/2", "podPhase", 6, ""}}}, }, { // Test container error overwrites pod phase @@ -1315,7 +1403,7 @@ func TestPrintPod(t *testing.T) { }, }, }, - "test2\t1/2\tContainerWaitingReason\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test2", "1/2", "ContainerWaitingReason", 6, ""}}}, }, { // Test the same as the above but with Terminated state and the first container overwrites the rest @@ -1330,7 +1418,7 @@ func TestPrintPod(t *testing.T) { }, }, }, - "test3\t0/2\tContainerWaitingReason\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test3", "0/2", "ContainerWaitingReason", 6, ""}}}, }, { // Test ready is not enough for reporting running @@ -1345,7 +1433,7 @@ func TestPrintPod(t *testing.T) { }, }, }, - "test4\t1/2\tpodPhase\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test4", "1/2", "podPhase", 6, ""}}}, }, { // Test ready is not enough for reporting running @@ -1361,25 +1449,28 @@ func TestPrintPod(t *testing.T) { }, }, }, - "test5\t1/2\tOutOfDisk\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test5", "1/2", "OutOfDisk", 6, ""}}}, }, } - buf := bytes.NewBuffer([]byte{}) - for _, test := range tests { - printPod(&test.pod, buf, printers.PrintOptions{ShowAll: true}) - // We ignore time - if !strings.HasPrefix(buf.String(), test.expect) { - t.Fatalf("Expected: %s, got: %s", test.expect, buf.String()) + for i, test := range tests { + rows, err := printPod(&test.pod, printers.PrintOptions{ShowAll: true}) + if err != nil { + t.Fatal(err) + } + for i := range rows { + rows[i].Object.Object = nil + } + if !reflect.DeepEqual(test.expect, rows) { + t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expect, rows)) } - buf.Reset() } } func TestPrintNonTerminatedPod(t *testing.T) { tests := []struct { pod api.Pod - expect string + expect []metav1alpha1.TableRow }{ { // Test pod phase Running should be printed @@ -1394,7 +1485,7 @@ func TestPrintNonTerminatedPod(t *testing.T) { }, }, }, - "test1\t1/2\tRunning\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test1", "1/2", "Running", 6, ""}}}, }, { // Test pod phase Pending should be printed @@ -1409,7 +1500,7 @@ func TestPrintNonTerminatedPod(t *testing.T) { }, }, }, - "test2\t1/2\tPending\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test2", "1/2", "Pending", 6, ""}}}, }, { // Test pod phase Unknown should be printed @@ -1424,7 +1515,7 @@ func TestPrintNonTerminatedPod(t *testing.T) { }, }, }, - "test3\t1/2\tUnknown\t6\t", + []metav1alpha1.TableRow{{Cells: []interface{}{"test3", "1/2", "Unknown", 6, ""}}}, }, { // Test pod phase Succeeded shouldn't be printed @@ -1439,7 +1530,7 @@ func TestPrintNonTerminatedPod(t *testing.T) { }, }, }, - "", + []metav1alpha1.TableRow{{Cells: []interface{}{"test4", "1/2", "Succeeded", 6, ""}, Conditions: podSuccessConditions}}, }, { // Test pod phase Failed shouldn't be printed @@ -1454,18 +1545,22 @@ func TestPrintNonTerminatedPod(t *testing.T) { }, }, }, - "", + []metav1alpha1.TableRow{{Cells: []interface{}{"test5", "1/2", "Failed", 6, ""}, Conditions: podFailedConditions}}, }, } - buf := bytes.NewBuffer([]byte{}) - for _, test := range tests { - printPod(&test.pod, buf, printers.PrintOptions{}) - // We ignore time - if !strings.HasPrefix(buf.String(), test.expect) { - t.Fatalf("Expected: %s, got: %s", test.expect, buf.String()) + for i, test := range tests { + table, err := printers.NewTablePrinter().With(AddHandlers).PrintTable(&test.pod, printers.PrintOptions{}) + if err != nil { + t.Fatal(err) + } + rows := table.Rows + for i := range rows { + rows[i].Object.Object = nil + } + if !reflect.DeepEqual(test.expect, rows) { + t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expect, rows)) } - buf.Reset() } } @@ -1473,8 +1568,7 @@ func TestPrintPodWithLabels(t *testing.T) { tests := []struct { pod api.Pod labelColumns []string - startsWith string - endsWith string + expect []metav1alpha1.TableRow }{ { // Test name, num of containers, restarts, container ready status @@ -1493,8 +1587,7 @@ func TestPrintPodWithLabels(t *testing.T) { }, }, []string{"col1", "COL2"}, - "test1\t1/2\tpodPhase\t6\t", - "\tasd\tzxc\n", + []metav1alpha1.TableRow{{Cells: []interface{}{"test1", "1/2", "podPhase", 6, "", "asd", "zxc"}}}, }, { // Test name, num of containers, restarts, container ready status @@ -1513,19 +1606,22 @@ func TestPrintPodWithLabels(t *testing.T) { }, }, []string{}, - "test1\t1/2\tpodPhase\t6\t", - "\n", + []metav1alpha1.TableRow{{Cells: []interface{}{"test1", "1/2", "podPhase", 6, ""}}}, }, } - buf := bytes.NewBuffer([]byte{}) - for _, test := range tests { - printPod(&test.pod, buf, printers.PrintOptions{ColumnLabels: test.labelColumns}) - // We ignore time - if !strings.HasPrefix(buf.String(), test.startsWith) || !strings.HasSuffix(buf.String(), test.endsWith) { - t.Fatalf("Expected to start with: %s and end with: %s, but got: %s", test.startsWith, test.endsWith, buf.String()) + for i, test := range tests { + table, err := printers.NewTablePrinter().With(AddHandlers).PrintTable(&test.pod, printers.PrintOptions{ColumnLabels: test.labelColumns}) + if err != nil { + t.Fatal(err) + } + rows := table.Rows + for i := range rows { + rows[i].Object.Object = nil + } + if !reflect.DeepEqual(test.expect, rows) { + t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expect, rows)) } - buf.Reset() } } @@ -2078,9 +2174,8 @@ func TestPrintHPA(t *testing.T) { func TestPrintPodShowLabels(t *testing.T) { tests := []struct { pod api.Pod - startsWith string - endsWith string showLabels bool + expect []metav1alpha1.TableRow }{ { // Test name, num of containers, restarts, container ready status @@ -2098,9 +2193,8 @@ func TestPrintPodShowLabels(t *testing.T) { }, }, }, - "test1\t1/2\tpodPhase\t6\t", - "\tCOL2=zxc,col1=asd\n", true, + []metav1alpha1.TableRow{{Cells: []interface{}{"test1", "1/2", "podPhase", 6, "", "COL2=zxc,col1=asd"}}}, }, { // Test name, num of containers, restarts, container ready status @@ -2118,20 +2212,23 @@ func TestPrintPodShowLabels(t *testing.T) { }, }, }, - "test1\t1/2\tpodPhase\t6\t", - "\n", false, + []metav1alpha1.TableRow{{Cells: []interface{}{"test1", "1/2", "podPhase", 6, ""}}}, }, } - buf := bytes.NewBuffer([]byte{}) - for _, test := range tests { - printPod(&test.pod, buf, printers.PrintOptions{ShowLabels: test.showLabels}) - // We ignore time - if !strings.HasPrefix(buf.String(), test.startsWith) || !strings.HasSuffix(buf.String(), test.endsWith) { - t.Fatalf("Expected to start with: %s and end with: %s, but got: %s", test.startsWith, test.endsWith, buf.String()) + for i, test := range tests { + table, err := printers.NewTablePrinter().With(AddHandlers).PrintTable(&test.pod, printers.PrintOptions{ShowLabels: test.showLabels}) + if err != nil { + t.Fatal(err) + } + rows := table.Rows + for i := range rows { + rows[i].Object.Object = nil + } + if !reflect.DeepEqual(test.expect, rows) { + t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expect, rows)) } - buf.Reset() } } diff --git a/pkg/printers/storage/BUILD b/pkg/printers/storage/BUILD new file mode 100644 index 00000000000..c5478f458e7 --- /dev/null +++ b/pkg/printers/storage/BUILD @@ -0,0 +1,33 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = ["storage.go"], + tags = ["automanaged"], + deps = [ + "//pkg/printers:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/printers/storage/storage.go b/pkg/printers/storage/storage.go new file mode 100644 index 00000000000..e70f408c73f --- /dev/null +++ b/pkg/printers/storage/storage.go @@ -0,0 +1,32 @@ +/* +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 storage + +import ( + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" + "k8s.io/apimachinery/pkg/runtime" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kubernetes/pkg/printers" +) + +type TableConvertor struct { + printers.TablePrinter +} + +func (c TableConvertor) ConvertToTable(ctx genericapirequest.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) { + return c.TablePrinter.PrintTable(obj, printers.PrintOptions{Wide: true}) +} diff --git a/pkg/registry/core/pod/storage/BUILD b/pkg/registry/core/pod/storage/BUILD index 5fd5a25cc6a..0079c51606c 100644 --- a/pkg/registry/core/pod/storage/BUILD +++ b/pkg/registry/core/pod/storage/BUILD @@ -15,12 +15,14 @@ go_test( tags = ["automanaged"], deps = [ "//pkg/api:go_default_library", + "//pkg/api/v1:go_default_library", "//pkg/registry/registrytest:go_default_library", "//pkg/securitycontext:go_default_library", "//vendor/golang.org/x/net/context:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", @@ -49,6 +51,9 @@ go_library( "//pkg/client/clientset_generated/internalclientset/typed/policy/internalversion:go_default_library", "//pkg/client/retry:go_default_library", "//pkg/kubelet/client:go_default_library", + "//pkg/printers:go_default_library", + "//pkg/printers/internalversion:go_default_library", + "//pkg/printers/storage:go_default_library", "//pkg/registry/cachesize:go_default_library", "//pkg/registry/core/pod:go_default_library", "//pkg/registry/core/pod/rest:go_default_library", diff --git a/pkg/registry/core/pod/storage/storage.go b/pkg/registry/core/pod/storage/storage.go index b6955d503b4..d4107fff034 100644 --- a/pkg/registry/core/pod/storage/storage.go +++ b/pkg/registry/core/pod/storage/storage.go @@ -35,6 +35,9 @@ import ( "k8s.io/kubernetes/pkg/api/validation" policyclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/policy/internalversion" "k8s.io/kubernetes/pkg/kubelet/client" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" "k8s.io/kubernetes/pkg/registry/cachesize" "k8s.io/kubernetes/pkg/registry/core/pod" podrest "k8s.io/kubernetes/pkg/registry/core/pod/rest" @@ -61,6 +64,7 @@ type REST struct { // NewStorage returns a RESTStorage object that will work against pods. func NewStorage(optsGetter generic.RESTOptionsGetter, k client.ConnectionInfoGetter, proxyTransport http.RoundTripper, podDisruptionBudgetClient policyclient.PodDisruptionBudgetsGetter) PodStorage { + store := &genericregistry.Store{ Copier: api.Scheme, NewFunc: func() runtime.Object { return &api.Pod{} }, @@ -73,6 +77,8 @@ func NewStorage(optsGetter generic.RESTOptionsGetter, k client.ConnectionInfoGet UpdateStrategy: pod.Strategy, DeleteStrategy: pod.Strategy, ReturnDeletedObject: true, + + TableConvertor: printerstorage.TableConvertor{TablePrinter: printers.NewTablePrinter().With(printersinternal.AddHandlers)}, } options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: pod.GetAttrs, TriggerFunc: pod.NodeNameTriggerFunc} if err := store.CompleteWithOptions(options); err != nil { diff --git a/pkg/registry/core/pod/storage/storage_test.go b/pkg/registry/core/pod/storage/storage_test.go index 88d469f43ca..726d19afa03 100644 --- a/pkg/registry/core/pod/storage/storage_test.go +++ b/pkg/registry/core/pod/storage/storage_test.go @@ -19,11 +19,13 @@ package storage import ( "strings" "testing" + "time" "golang.org/x/net/context" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -35,6 +37,7 @@ import ( storeerr "k8s.io/apiserver/pkg/storage/errors" etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/registry/registrytest" "k8s.io/kubernetes/pkg/securitycontext" ) @@ -396,6 +399,88 @@ func TestWatch(t *testing.T) { ) } +func TestConvertToTableList(t *testing.T) { + storage, _, _, server := newStorage(t) + defer server.Terminate(t) + defer storage.Store.DestroyFunc() + ctx := genericapirequest.NewDefaultContext() + + columns := []metav1alpha1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Ready", Type: "string", Description: "The aggregate readiness state of this pod for accepting traffic."}, + {Name: "Status", Type: "string", Description: "The aggregate status of the containers in this pod."}, + {Name: "Restarts", Type: "integer", Description: "The number of times the containers in this pod have been restarted."}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + {Name: "IP", Type: "string", Priority: 1, Description: v1.PodStatus{}.SwaggerDoc()["podIP"]}, + {Name: "Node", Type: "string", Priority: 1, Description: v1.PodSpec{}.SwaggerDoc()["nodeName"]}, + } + + pod1 := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "foo", CreationTimestamp: metav1.NewTime(time.Now().Add(-370 * 24 * time.Hour))}, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: "ctr1"}, + {Name: "ctr2", Ports: []api.ContainerPort{{ContainerPort: 9376}}}, + }, + NodeName: "test-node", + }, + Status: api.PodStatus{ + PodIP: "10.1.2.3", + Phase: api.PodPending, + ContainerStatuses: []api.ContainerStatus{ + {Name: "ctr1", State: api.ContainerState{Running: &api.ContainerStateRunning{}}, RestartCount: 10, Ready: true}, + {Name: "ctr2", State: api.ContainerState{Waiting: &api.ContainerStateWaiting{}}, RestartCount: 0}, + }, + }, + } + + testCases := []struct { + in runtime.Object + out *metav1alpha1.Table + err bool + }{ + { + in: nil, + err: true, + }, + { + in: &api.Pod{}, + out: &metav1alpha1.Table{ + ColumnDefinitions: columns, + Rows: []metav1alpha1.TableRow{ + {Cells: []interface{}{"", "0/0", "", 0, "", "", ""}, Object: runtime.RawExtension{Object: &api.Pod{}}}, + }, + }, + }, + { + in: pod1, + out: &metav1alpha1.Table{ + ColumnDefinitions: columns, + Rows: []metav1alpha1.TableRow{ + {Cells: []interface{}{"foo", "1/2", "Pending", 10, "1y", "10.1.2.3", "test-node"}, Object: runtime.RawExtension{Object: pod1}}, + }, + }, + }, + { + in: &api.PodList{}, + out: &metav1alpha1.Table{ColumnDefinitions: columns}, + }, + } + for i, test := range testCases { + out, err := storage.ConvertToTable(ctx, test.in, nil) + if err != nil { + if test.err { + continue + } + t.Errorf("%d: error: %v", i, err) + continue + } + if !apiequality.Semantic.DeepEqual(test.out, out) { + t.Errorf("%d: mismatch: %s", i, diff.ObjectReflectDiff(test.out, out)) + } + } +} + func TestEtcdCreate(t *testing.T) { storage, bindingStorage, _, server := newStorage(t) defer server.Terminate(t) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD index f6d13979840..b148d5cee9e 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD @@ -11,6 +11,7 @@ load( go_test( name = "go_default_test", srcs = [ + "meta_test.go", "multirestmapper_test.go", "priority_test.go", "restmapper_test.go", @@ -18,8 +19,12 @@ go_test( library = ":go_default_library", tags = ["automanaged"], deps = [ + "//vendor/github.com/google/gofuzz:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", ], ) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go index 01eb0503137..45d850ea8a7 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go @@ -114,6 +114,7 @@ func AsPartialObjectMetadata(m metav1.Object) *metav1alpha1.PartialObjectMetadat OwnerReferences: m.GetOwnerReferences(), Finalizers: m.GetFinalizers(), ClusterName: m.GetClusterName(), + Initializers: m.GetInitializers(), }, } } diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta_test.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta_test.go new file mode 100644 index 00000000000..c7b753e0f54 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta_test.go @@ -0,0 +1,51 @@ +/* +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 meta + +import ( + "math/rand" + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" + "k8s.io/apimachinery/pkg/util/diff" + + fuzz "github.com/google/gofuzz" +) + +func TestAsPartialObjectMetadata(t *testing.T) { + f := fuzz.New().NilChance(.5).NumElements(0, 1).RandSource(rand.NewSource(1)) + + for i := 0; i < 100; i++ { + m := &metav1.ObjectMeta{} + f.Fuzz(m) + partial := AsPartialObjectMetadata(m) + if !reflect.DeepEqual(&partial.ObjectMeta, m) { + t.Fatalf("incomplete partial object metadata: %s", diff.ObjectReflectDiff(&partial.ObjectMeta, m)) + } + } + + for i := 0; i < 100; i++ { + m := &metav1alpha1.PartialObjectMetadata{} + f.Fuzz(&m.ObjectMeta) + partial := AsPartialObjectMetadata(m) + if !reflect.DeepEqual(&partial.ObjectMeta, &m.ObjectMeta) { + t.Fatalf("incomplete partial object metadata: %s", diff.ObjectReflectDiff(&partial.ObjectMeta, &m.ObjectMeta)) + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go index c00eafcc56b..0ee7d99ca17 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go @@ -149,6 +149,9 @@ func (meta *ObjectMeta) GetFinalizers() []string { return m func (meta *ObjectMeta) SetFinalizers(finalizers []string) { meta.Finalizers = finalizers } func (meta *ObjectMeta) GetOwnerReferences() []OwnerReference { + if meta.OwnerReferences == nil { + return nil + } ret := make([]OwnerReference, len(meta.OwnerReferences)) for i := 0; i < len(meta.OwnerReferences); i++ { ret[i].Kind = meta.OwnerReferences[i].Kind @@ -168,6 +171,10 @@ func (meta *ObjectMeta) GetOwnerReferences() []OwnerReference { } func (meta *ObjectMeta) SetOwnerReferences(references []OwnerReference) { + if references == nil { + meta.OwnerReferences = nil + return + } newReferences := make([]OwnerReference, len(references)) for i := 0; i < len(references); i++ { newReferences[i].Kind = references[i].Kind diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go index 70aff719880..1aec3ba2d2c 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go @@ -34,7 +34,7 @@ import ( // transformResponseObject takes an object loaded from storage and performs any necessary transformations. // Will write the complete response object. func transformResponseObject(ctx request.Context, scope RequestScope, req *http.Request, w http.ResponseWriter, statusCode int, result runtime.Object) { - // TODO: use returned serializer + // TODO: fetch the media type much earlier in request processing and pass it into this method. mediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, &scope) if err != nil { status := responsewriters.ErrorToAPIStatus(err) @@ -169,7 +169,7 @@ func transformResponseObject(ctx request.Context, scope RequestScope, req *http. } } - responsewriters.WriteObject(statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + responsewriters.WriteObject(ctx, statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) } // errNotAcceptable indicates Accept negotiation has failed diff --git a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/BUILD b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/BUILD index 32697b60893..15a507c6638 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/BUILD @@ -62,6 +62,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/api/validation/path:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go index a6867426c5d..ff81d0b2f90 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/api/validation/path" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -155,6 +156,9 @@ type Store struct { // ExportStrategy implements resource-specific behavior during export, // optional. Exported objects are not decorated. ExportStrategy rest.RESTExportStrategy + // TableConvertor is an optional interface for transforming items or lists + // of items into tabular output. If unset, the default will be used. + TableConvertor rest.TableConvertor // Storage is the interface for the underlying storage for the resource. Storage storage.Interface @@ -169,6 +173,7 @@ type Store struct { // Note: the rest.StandardStorage interface aggregates the common REST verbs var _ rest.StandardStorage = &Store{} var _ rest.Exporter = &Store{} +var _ rest.TableConvertor = &Store{} const OptimisticLockErrorMsg = "the object has been modified; please apply your changes to the latest version and try again" @@ -1230,3 +1235,10 @@ func (e *Store) CompleteWithOptions(options *generic.StoreOptions) error { return nil } + +func (e *Store) ConvertToTable(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) { + if e.TableConvertor != nil { + return e.TableConvertor.ConvertToTable(ctx, object, tableOptions) + } + return rest.DefaultTableConvertor.ConvertToTable(ctx, object, tableOptions) +} diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD b/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD index 3911586ccdf..7dcca50ca62 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/BUILD @@ -31,6 +31,7 @@ go_library( "export.go", "meta.go", "rest.go", + "table.go", "update.go", ], tags = ["automanaged"], @@ -42,6 +43,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/uuid:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go index 262e05697fa..015754f8602 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go @@ -118,7 +118,7 @@ type GetterWithOptions interface { } type TableConvertor interface { - ConvertToTableList(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.TableList, error) + ConvertToTable(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) } // Deleter is an object that can delete a named RESTful resource. diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go index 4b48711d37f..cc1e83d2bdf 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go @@ -32,15 +32,15 @@ type defaultTableConvertor struct{} var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc() -func (defaultTableConvertor) ConvertToTableList(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.TableList, error) { - var table metav1alpha1.TableList +func (defaultTableConvertor) ConvertToTable(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) { + var table metav1alpha1.Table fn := func(obj runtime.Object) error { m, err := meta.Accessor(obj) if err != nil { // TODO: skip objects we don't recognize return nil } - table.Items = append(table.Items, metav1alpha1.TableListItem{ + table.Rows = append(table.Rows, metav1alpha1.TableRow{ Cells: []interface{}{m.GetClusterName(), m.GetNamespace(), m.GetName(), m.GetCreationTimestamp().Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Object: obj}, }) @@ -56,7 +56,7 @@ func (defaultTableConvertor) ConvertToTableList(ctx genericapirequest.Context, o return nil, err } } - table.Headers = []metav1alpha1.TableListHeader{ + table.ColumnDefinitions = []metav1alpha1.TableColumnDefinition{ {Name: "Cluster Name", Type: "string", Description: swaggerMetadataDescriptions["clusterName"]}, {Name: "Namespace", Type: "string", Description: swaggerMetadataDescriptions["namespace"]}, {Name: "Name", Type: "string", Description: swaggerMetadataDescriptions["name"]}, @@ -71,8 +71,8 @@ func (defaultTableConvertor) ConvertToTableList(ctx genericapirequest.Context, o return &table, nil } -func trimColumn(column int, table *metav1alpha1.TableList) bool { - for _, item := range table.Items { +func trimColumn(column int, table *metav1alpha1.Table) bool { + for _, item := range table.Rows { switch t := item.Cells[column].(type) { case string: if len(t) > 0 { @@ -85,22 +85,22 @@ func trimColumn(column int, table *metav1alpha1.TableList) bool { } } if column == 0 { - table.Headers = table.Headers[1:] + table.ColumnDefinitions = table.ColumnDefinitions[1:] } else { - for j := column; j < len(table.Headers); j++ { - table.Headers[j] = table.Headers[j+1] + for j := column; j < len(table.ColumnDefinitions); j++ { + table.ColumnDefinitions[j] = table.ColumnDefinitions[j+1] } } - for i := range table.Items { - cells := table.Items[i].Cells + for i := range table.Rows { + cells := table.Rows[i].Cells if column == 0 { - table.Items[i].Cells = cells[1:] + table.Rows[i].Cells = cells[1:] continue } for j := column; j < len(cells); j++ { cells[j] = cells[j+1] } - table.Items[i].Cells = cells[:len(cells)-1] + table.Rows[i].Cells = cells[:len(cells)-1] } return true } diff --git a/staging/src/k8s.io/client-go/Godeps/Godeps.json b/staging/src/k8s.io/client-go/Godeps/Godeps.json index bc4dcc53c12..d6b84408d6a 100644 --- a/staging/src/k8s.io/client-go/Godeps/Godeps.json +++ b/staging/src/k8s.io/client-go/Godeps/Godeps.json @@ -330,6 +330,10 @@ "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1alpha1", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apimachinery/pkg/conversion", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"