Refactor printers to support rendering as a Table

Return tables from the server.
pull/6/head
Clayton Coleman 2017-05-26 19:00:01 -04:00
parent f203e42cb9
commit 7ce63eb608
No known key found for this signature in database
GPG Key ID: 3D16906B4F1C5CB3
22 changed files with 951 additions and 161 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = "<none>"
}
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 {

View File

@ -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<unknown>\n",
},
{
obj: runningPod, opts: printers.PrintOptions{WithKind: true, Kind: "pods"},
expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\npods/test1\t1/2\tRunning\t6\t<unknown>\n",
},
{
obj: runningPod, opts: printers.PrintOptions{ShowLabels: true},
expect: "NAME\tREADY\tSTATUS\tRESTARTS\tAGE\tLABELS\ntest1\t1/2\tRunning\t6\t<unknown>\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<unknown>\t1\ntest2\t1/2\tFailed\t6\t<unknown>\t\n",
},
{
obj: runningPod, opts: printers.PrintOptions{NoHeaders: true},
expect: "test1\t1/2\tRunning\t6\t<unknown>\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<unknown>\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, "<unknown>"}}},
},
{
// 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, "<unknown>"}}},
},
{
// 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, "<unknown>"}}},
},
{
// 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, "<unknown>"}}},
},
{
// 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, "<unknown>"}}},
},
}
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, "<unknown>"}}},
},
{
// 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, "<unknown>"}}},
},
{
// 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, "<unknown>"}}},
},
{
// 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, "<unknown>"}, 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, "<unknown>"}, 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, "<unknown>", "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, "<unknown>"}}},
},
}
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, "<unknown>", "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, "<unknown>"}}},
},
}
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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, "<unknown>", "<none>", "<none>"}, 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)

View File

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

View File

@ -114,6 +114,7 @@ func AsPartialObjectMetadata(m metav1.Object) *metav1alpha1.PartialObjectMetadat
OwnerReferences: m.GetOwnerReferences(),
Finalizers: m.GetFinalizers(),
ClusterName: m.GetClusterName(),
Initializers: m.GetInitializers(),
},
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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