diff --git a/hack/lib/util.sh b/hack/lib/util.sh index f2af8e62ffd6b..50f6b55572a7a 100755 --- a/hack/lib/util.sh +++ b/hack/lib/util.sh @@ -334,6 +334,12 @@ kube::util::group-version-to-pkg-path() { meta/v1) echo "../vendor/k8s.io/apimachinery/pkg/apis/meta/v1" ;; + meta/v1alpha1) + echo "vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1" + ;; + meta/v1alpha1) + echo "../vendor/k8s.io/apimachinery/pkg/apis/meta/v1alpha1" + ;; unversioned) echo "pkg/api/unversioned" ;; diff --git a/hack/update-generated-swagger-docs.sh b/hack/update-generated-swagger-docs.sh index 3fa23dfe0ff3f..44e5458bbcc57 100755 --- a/hack/update-generated-swagger-docs.sh +++ b/hack/update-generated-swagger-docs.sh @@ -28,7 +28,7 @@ source "${KUBE_ROOT}/hack/lib/swagger.sh" kube::golang::setup_env -GROUP_VERSIONS=(meta/v1 ${KUBE_AVAILABLE_GROUP_VERSIONS}) +GROUP_VERSIONS=(meta/v1 meta/v1alpha1 ${KUBE_AVAILABLE_GROUP_VERSIONS}) # To avoid compile errors, remove the currently existing files. for group_version in "${GROUP_VERSIONS[@]}"; do diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 6fec9f56fa4d1..c4e2f3a6e640b 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 8e6e4b4da2b9a..c888148e23b2c 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 8517ef7ce3dbe..83fda3a67a833 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 } -// validatePrintHandlerFunc validates print handler signature. +// 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. // 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 adfa6ea7e8a95..616805dd7eb87 100644 --- a/pkg/printers/internalversion/BUILD +++ b/pkg/printers/internalversion/BUILD @@ -41,6 +41,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", @@ -66,6 +67,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", @@ -96,8 +98,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 682ab2b872501..84d99d2f898e8 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" @@ -54,8 +57,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"} @@ -107,27 +108,21 @@ var ( controllerRevisionColumns = []string{"NAME", "CONTROLLER", "REVISION", "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) @@ -251,10 +246,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 @@ -264,6 +273,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] @@ -320,21 +340,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 @@ -345,22 +351,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 e55071b947e7b..51e5eb6e3036e 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" @@ -268,7 +270,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()) } @@ -286,7 +288,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()) } @@ -1253,7 +1255,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{ @@ -1267,7 +1269,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. @@ -1283,10 +1285,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 @@ -1301,7 +1389,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 @@ -1316,7 +1404,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 @@ -1331,7 +1419,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 @@ -1346,7 +1434,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 @@ -1362,25 +1450,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 @@ -1395,7 +1486,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 @@ -1410,7 +1501,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 @@ -1425,7 +1516,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 @@ -1440,7 +1531,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 @@ -1455,18 +1546,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() } } @@ -1474,8 +1569,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 @@ -1494,8 +1588,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 @@ -1514,19 +1607,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() } } @@ -2079,9 +2175,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 @@ -2099,9 +2194,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 @@ -2119,20 +2213,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 0000000000000..c5478f458e711 --- /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 0000000000000..e70f408c73ffa --- /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 5fd5a25cc6a56..0079c51606c87 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 b6955d503b4e9..d4107fff0348a 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 88d469f43ca6f..726d19afa0348 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 f4b3598def063..b148d5cee9ed5 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", ], ) @@ -43,6 +48,7 @@ go_library( "//vendor/github.com/golang/glog: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/conversion:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema: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 9cc3729741491..45d850ea8a7e4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go @@ -23,6 +23,7 @@ import ( "github.com/golang/glog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1alpha1 "k8s.io/apimachinery/pkg/apis/meta/v1alpha1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -89,6 +90,36 @@ func Accessor(obj interface{}) (metav1.Object, error) { } } +// AsPartialObjectMetadata takes the metav1 interface and returns a partial object. +// TODO: consider making this solely a conversion action. +func AsPartialObjectMetadata(m metav1.Object) *metav1alpha1.PartialObjectMetadata { + switch t := m.(type) { + case *metav1.ObjectMeta: + return &metav1alpha1.PartialObjectMetadata{ObjectMeta: *t} + default: + return &metav1alpha1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: m.GetName(), + GenerateName: m.GetGenerateName(), + Namespace: m.GetNamespace(), + SelfLink: m.GetSelfLink(), + UID: m.GetUID(), + ResourceVersion: m.GetResourceVersion(), + Generation: m.GetGeneration(), + CreationTimestamp: m.GetCreationTimestamp(), + DeletionTimestamp: m.GetDeletionTimestamp(), + DeletionGracePeriodSeconds: m.GetDeletionGracePeriodSeconds(), + Labels: m.GetLabels(), + Annotations: m.GetAnnotations(), + OwnerReferences: m.GetOwnerReferences(), + Finalizers: m.GetFinalizers(), + ClusterName: m.GetClusterName(), + Initializers: m.GetInitializers(), + }, + } + } +} + // TypeAccessor returns an interface that allows retrieving and modifying the APIVersion // and Kind of an in-memory internal object. // TODO: this interface is used to test code that does not have ObjectMeta or ListMeta 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 0000000000000..c7b753e0f5474 --- /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/internalversion/register.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion/register.go index 95dcea915c268..bf4f4cd6b988f 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion/register.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion/register.go @@ -72,9 +72,15 @@ func addToGroupVersion(scheme *runtime.Scheme, groupVersion schema.GroupVersion) ) scheme.AddKnownTypes(SchemeGroupVersion, &metav1alpha1.Table{}, + &metav1alpha1.TableOptions{}, + &metav1alpha1.PartialObjectMetadata{}, + &metav1alpha1.PartialObjectMetadataList{}, ) scheme.AddKnownTypes(metav1alpha1.SchemeGroupVersion, &metav1alpha1.Table{}, + &metav1alpha1.TableOptions{}, + &metav1alpha1.PartialObjectMetadata{}, + &metav1alpha1.PartialObjectMetadataList{}, ) // Allow delete options to be decoded across all version in this scheme (we may want to be more clever than this) scheme.AddUnversionedTypes(SchemeGroupVersion, &metav1.DeleteOptions{}) 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 c00eafcc56b90..0ee7d99ca1733 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/apimachinery/pkg/apis/meta/v1alpha1/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/BUILD index dbcfb8204c34e..f68aa81de7688 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/BUILD @@ -14,6 +14,7 @@ go_library( "generated.pb.go", "register.go", "types.go", + "types_swagger_doc_generated.go", "zz_generated.deepcopy.go", ], tags = ["automanaged"], diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.pb.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.pb.go index 5130bb56ac207..de42334481b96 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.pb.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.pb.go @@ -26,6 +26,7 @@ limitations under the License. It has these top-level messages: PartialObjectMetadata + PartialObjectMetadataList TableOptions */ package v1alpha1 @@ -54,12 +55,19 @@ func (m *PartialObjectMetadata) Reset() { *m = PartialObjectM func (*PartialObjectMetadata) ProtoMessage() {} func (*PartialObjectMetadata) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{0} } +func (m *PartialObjectMetadataList) Reset() { *m = PartialObjectMetadataList{} } +func (*PartialObjectMetadataList) ProtoMessage() {} +func (*PartialObjectMetadataList) Descriptor() ([]byte, []int) { + return fileDescriptorGenerated, []int{1} +} + func (m *TableOptions) Reset() { *m = TableOptions{} } func (*TableOptions) ProtoMessage() {} -func (*TableOptions) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{1} } +func (*TableOptions) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{2} } func init() { proto.RegisterType((*PartialObjectMetadata)(nil), "k8s.io.apimachinery.pkg.apis.meta.v1alpha1.PartialObjectMetadata") + proto.RegisterType((*PartialObjectMetadataList)(nil), "k8s.io.apimachinery.pkg.apis.meta.v1alpha1.PartialObjectMetadataList") proto.RegisterType((*TableOptions)(nil), "k8s.io.apimachinery.pkg.apis.meta.v1alpha1.TableOptions") } func (m *PartialObjectMetadata) Marshal() (dAtA []byte, err error) { @@ -88,6 +96,36 @@ func (m *PartialObjectMetadata) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *PartialObjectMetadataList) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *PartialObjectMetadataList) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Items) > 0 { + for _, msg := range m.Items { + dAtA[i] = 0xa + i++ + i = encodeVarintGenerated(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + return i, nil +} + func (m *TableOptions) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -145,6 +183,18 @@ func (m *PartialObjectMetadata) Size() (n int) { return n } +func (m *PartialObjectMetadataList) Size() (n int) { + var l int + _ = l + if len(m.Items) > 0 { + for _, e := range m.Items { + l = e.Size() + n += 1 + l + sovGenerated(uint64(l)) + } + } + return n +} + func (m *TableOptions) Size() (n int) { var l int _ = l @@ -176,6 +226,16 @@ func (this *PartialObjectMetadata) String() string { }, "") return s } +func (this *PartialObjectMetadataList) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&PartialObjectMetadataList{`, + `Items:` + strings.Replace(fmt.Sprintf("%v", this.Items), "PartialObjectMetadata", "PartialObjectMetadata", 1) + `,`, + `}`, + }, "") + return s +} func (this *TableOptions) String() string { if this == nil { return "nil" @@ -274,6 +334,87 @@ func (m *PartialObjectMetadata) Unmarshal(dAtA []byte) error { } return nil } +func (m *PartialObjectMetadataList) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: PartialObjectMetadataList: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: PartialObjectMetadataList: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Items", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Items = append(m.Items, &PartialObjectMetadata{}) + if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *TableOptions) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -463,27 +604,30 @@ func init() { } var fileDescriptorGenerated = []byte{ - // 344 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0x3f, 0x4f, 0xf3, 0x30, - 0x10, 0x87, 0x93, 0xad, 0x6f, 0x5e, 0xba, 0x04, 0x21, 0xa1, 0x0e, 0x2e, 0xea, 0x84, 0x2a, 0xb0, - 0x29, 0x20, 0xc4, 0xdc, 0x8d, 0x01, 0xb5, 0x2a, 0x4c, 0x4c, 0x38, 0xc9, 0x91, 0x98, 0x24, 0x76, - 0x64, 0x5f, 0x2a, 0x75, 0xe3, 0x23, 0xf0, 0xb1, 0x3a, 0x76, 0x64, 0xaa, 0x68, 0xf8, 0x16, 0x4c, - 0xa8, 0x69, 0x4b, 0xff, 0xa9, 0xa2, 0xdb, 0xdd, 0xef, 0xf2, 0x3c, 0x39, 0x5b, 0x76, 0x1e, 0xe2, - 0x5b, 0x43, 0x85, 0x62, 0x71, 0xee, 0x81, 0x96, 0x80, 0x60, 0x58, 0x1f, 0x64, 0xa0, 0x34, 0x9b, - 0x0f, 0x78, 0x26, 0x52, 0xee, 0x47, 0x42, 0x82, 0x1e, 0xb0, 0x2c, 0x0e, 0xa7, 0x81, 0x61, 0x29, - 0x20, 0x67, 0xfd, 0x16, 0x4f, 0xb2, 0x88, 0xb7, 0x58, 0x08, 0x12, 0x34, 0x47, 0x08, 0x68, 0xa6, - 0x15, 0x2a, 0xb7, 0x39, 0x63, 0xe9, 0x2a, 0x4b, 0xb3, 0x38, 0x9c, 0x06, 0x86, 0x4e, 0x59, 0xba, - 0x60, 0x6b, 0xe7, 0xa1, 0xc0, 0x28, 0xf7, 0xa8, 0xaf, 0x52, 0x16, 0xaa, 0x50, 0xb1, 0x52, 0xe1, - 0xe5, 0x2f, 0x65, 0x57, 0x36, 0x65, 0x35, 0x53, 0xd7, 0xae, 0xf7, 0x59, 0x6b, 0x73, 0xa1, 0xda, - 0xce, 0xc3, 0xe8, 0x5c, 0xa2, 0x48, 0x61, 0x0b, 0xb8, 0xf9, 0x0b, 0x30, 0x7e, 0x04, 0x29, 0xdf, - 0xe2, 0xae, 0x76, 0x71, 0x39, 0x8a, 0x84, 0x09, 0x89, 0x06, 0xf5, 0x26, 0xd4, 0x18, 0x38, 0x47, - 0x5d, 0xae, 0x51, 0xf0, 0xa4, 0xe3, 0xbd, 0x82, 0x8f, 0xf7, 0x80, 0x3c, 0xe0, 0xc8, 0xdd, 0x67, - 0xa7, 0x92, 0xce, 0xeb, 0x63, 0xfb, 0xc4, 0x3e, 0xfd, 0x7f, 0x79, 0x41, 0xf7, 0xb9, 0x5a, 0xba, - 0xf4, 0xb4, 0xdd, 0xe1, 0xb8, 0x6e, 0x15, 0xe3, 0xba, 0xb3, 0xcc, 0x7a, 0xbf, 0xd6, 0x86, 0xe7, - 0x1c, 0x3c, 0x72, 0x2f, 0x81, 0x4e, 0x86, 0x42, 0x49, 0xe3, 0xf6, 0x9c, 0xaa, 0x90, 0x7e, 0x92, - 0x07, 0x30, 0xfb, 0xbc, 0xfc, 0xed, 0xbf, 0xf6, 0xd9, 0x5c, 0x52, 0xbd, 0x5b, 0x1d, 0x7e, 0x8f, - 0xeb, 0x87, 0x6b, 0x41, 0x57, 0x25, 0xc2, 0x1f, 0xf4, 0xd6, 0x15, 0xed, 0xe6, 0x70, 0x42, 0xac, - 0xd1, 0x84, 0x58, 0x1f, 0x13, 0x62, 0xbd, 0x15, 0xc4, 0x1e, 0x16, 0xc4, 0x1e, 0x15, 0xc4, 0xfe, - 0x2c, 0x88, 0xfd, 0xfe, 0x45, 0xac, 0xa7, 0xca, 0xe2, 0x35, 0xfc, 0x04, 0x00, 0x00, 0xff, 0xff, - 0x20, 0xf7, 0xa9, 0xe2, 0x8f, 0x02, 0x00, 0x00, + // 388 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x91, 0xbf, 0x6e, 0xd4, 0x40, + 0x10, 0x87, 0x6d, 0xa1, 0x48, 0xc9, 0x86, 0x34, 0x46, 0x48, 0xe1, 0x8a, 0x75, 0x74, 0x55, 0x14, + 0xc1, 0x2e, 0x09, 0x08, 0xd1, 0xe2, 0x2e, 0x12, 0x28, 0x91, 0xa1, 0xa2, 0x62, 0x6d, 0x0f, 0xf6, + 0x62, 0x7b, 0xd7, 0xda, 0x1d, 0x47, 0xba, 0x0a, 0x1e, 0x81, 0xc7, 0xba, 0x32, 0x25, 0x95, 0xc5, + 0x99, 0xb7, 0xa0, 0x42, 0xb6, 0x2f, 0xe4, 0xfe, 0x2a, 0xd7, 0xcd, 0xfc, 0x46, 0xdf, 0xe7, 0x19, + 0x2f, 0xf9, 0x98, 0xbf, 0xb5, 0x4c, 0x6a, 0x9e, 0xd7, 0x11, 0x18, 0x05, 0x08, 0x96, 0xdf, 0x80, + 0x4a, 0xb4, 0xe1, 0xf3, 0x81, 0xa8, 0x64, 0x29, 0xe2, 0x4c, 0x2a, 0x30, 0x13, 0x5e, 0xe5, 0x69, + 0x17, 0x58, 0x5e, 0x02, 0x0a, 0x7e, 0x73, 0x2e, 0x8a, 0x2a, 0x13, 0xe7, 0x3c, 0x05, 0x05, 0x46, + 0x20, 0x24, 0xac, 0x32, 0x1a, 0xb5, 0x77, 0x36, 0xb0, 0x6c, 0x91, 0x65, 0x55, 0x9e, 0x76, 0x81, + 0x65, 0x1d, 0xcb, 0xee, 0xd8, 0xd1, 0x8b, 0x54, 0x62, 0x56, 0x47, 0x2c, 0xd6, 0x25, 0x4f, 0x75, + 0xaa, 0x79, 0xaf, 0x88, 0xea, 0xaf, 0x7d, 0xd7, 0x37, 0x7d, 0x35, 0xa8, 0x47, 0xaf, 0x77, 0x59, + 0x6b, 0x75, 0xa1, 0xd1, 0xd6, 0x63, 0x4c, 0xad, 0x50, 0x96, 0xb0, 0x06, 0xbc, 0x79, 0x08, 0xb0, + 0x71, 0x06, 0xa5, 0x58, 0xe3, 0x5e, 0x6d, 0xe3, 0x6a, 0x94, 0x05, 0x97, 0x0a, 0x2d, 0x9a, 0x55, + 0x68, 0x3c, 0x21, 0x4f, 0xaf, 0x85, 0x41, 0x29, 0x8a, 0xab, 0xe8, 0x1b, 0xc4, 0xf8, 0x01, 0x50, + 0x24, 0x02, 0x85, 0xf7, 0x85, 0xec, 0x97, 0xf3, 0xfa, 0xd8, 0x3d, 0x71, 0x4f, 0x0f, 0x2f, 0x5e, + 0xb2, 0x5d, 0x7e, 0x2d, 0xbb, 0xf7, 0x04, 0xde, 0xb4, 0xf1, 0x9d, 0xb6, 0xf1, 0xc9, 0x7d, 0x16, + 0xfe, 0xb7, 0x8e, 0xbf, 0x93, 0x67, 0x1b, 0x3f, 0xfd, 0x5e, 0x5a, 0xf4, 0x22, 0xb2, 0x27, 0x11, + 0x4a, 0x7b, 0xec, 0x9e, 0x3c, 0x3a, 0x3d, 0xbc, 0x78, 0xc7, 0x76, 0x7f, 0x56, 0xb6, 0xd1, 0x1a, + 0x1c, 0xb4, 0x8d, 0xbf, 0x77, 0xd9, 0x39, 0xc3, 0x41, 0x3d, 0x8e, 0xc8, 0xe3, 0x4f, 0x22, 0x2a, + 0xe0, 0xaa, 0x42, 0xa9, 0x95, 0xf5, 0x42, 0x72, 0x24, 0x55, 0x5c, 0xd4, 0x09, 0x0c, 0x68, 0x7f, + 0xf7, 0x41, 0xf0, 0x7c, 0x7e, 0xc5, 0xd1, 0xe5, 0xe2, 0xf0, 0x6f, 0xe3, 0x3f, 0x59, 0x0a, 0xae, + 0x75, 0x21, 0xe3, 0x49, 0xb8, 0xac, 0x08, 0xce, 0xa6, 0x33, 0xea, 0xdc, 0xce, 0xa8, 0xf3, 0x6b, + 0x46, 0x9d, 0x1f, 0x2d, 0x75, 0xa7, 0x2d, 0x75, 0x6f, 0x5b, 0xea, 0xfe, 0x6e, 0xa9, 0xfb, 0xf3, + 0x0f, 0x75, 0x3e, 0xef, 0xdf, 0xed, 0xfd, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x08, 0x1a, 0xd9, 0x53, + 0x10, 0x03, 0x00, 0x00, } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.proto b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.proto index 22e2eb8534382..a3fb169c4a679 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.proto +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/generated.proto @@ -38,6 +38,12 @@ message PartialObjectMetadata { optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1; } +// PartialObjectMetadataList contains a list of objects containing only their metadata +message PartialObjectMetadataList { + // items contains each of the included items. + repeated PartialObjectMetadata items = 1; +} + // TableOptions are used when a Table is requested by the caller. message TableOptions { // includeObject decides whether to include each object along with its columnar information. diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/register.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/register.go index 9caf277179f6c..89f08f3837e65 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/register.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/register.go @@ -43,6 +43,7 @@ func init() { &Table{}, &TableOptions{}, &PartialObjectMetadata{}, + &PartialObjectMetadataList{}, ) // register manually. This usually goes through the SchemeBuilder, which we cannot use here. diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types.go index a6800fc177e36..c8ca4ea17b084 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types.go @@ -22,6 +22,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// TODO: Table does not generate to protobuf because of the interface{} - fix protobuf +// generation to support a meta type that can accept any valid JSON. + // Table is a tabular representation of a set of API resources. The server transforms the // object into a set of preferred columns for quickly reviewing the objects. // +protobuf=false @@ -30,28 +33,33 @@ type Table struct { // Standard list metadata. // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds // +optional - v1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + v1.ListMeta `json:"metadata,omitempty"` // columnDefinitions describes each column in the returned items array. The number of cells per row // will always match the number of column definitions. - ColumnDefinitions []TableColumnDefinitions `json:"columnDefinitions"` + ColumnDefinitions []TableColumnDefinition `json:"columnDefinitions"` // rows is the list of items in the table. Rows []TableRow `json:"rows"` } -// TableColumnDefinitions contains information about a column returned in the Table. +// TableColumnDefinition contains information about a column returned in the Table. // +protobuf=false -type TableColumnDefinitions struct { +type TableColumnDefinition struct { // name is a human readable name for the column. Name string `json:"name"` // type is an OpenAPI type definition for this column. // See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more. Type string `json:"type"` - // format is an optional OpenAPI type definition for this column. + // format is an optional OpenAPI type definition for this column. The 'name' format is applied + // to the primary identifier column to assist in clients identifying column is the resource name. // See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more. Format string `json:"format"` // description is a human readable description of this column. Description string `json:"description"` + // priority is an integer defining the relative importance of this column compared to others. Lower + // numbers are considered higher priority. Columns that may be omitted in limited space scenarios + // should be given a higher priority. + Priority int32 `json:"priority"` } // TableRow is an individual row in a table. @@ -60,13 +68,54 @@ type TableRow struct { // cells will be as wide as headers and may contain strings, numbers, booleans, simple maps, or lists, or // null. See the type field of the column definition for a more detailed description. Cells []interface{} `json:"cells"` + // conditions describe additional status of a row that are relevant for a human user. + // +optional + Conditions []TableRowCondition `json:"conditions,omitempty"` // This field contains the requested additional information about each object based on the includeObject // policy when requesting the Table. If "None", this field is empty, if "Object" this will be the // default serialization of the object for the current API version, and if "Metadata" (the default) will // contain the object metadata. Check the returned kind and apiVersion of the object before parsing. - Object runtime.RawExtension `json:"object"` + // +optional + Object runtime.RawExtension `json:"object,omitempty"` +} + +// TableRowCondition allows a row to be marked with additional information. +// +protobuf=false +type TableRowCondition struct { + // Type of row condition. + Type RowConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status ConditionStatus `json:"status"` + // (brief) machine readable reason for the condition's last transition. + // +optional + Reason string `json:"reason,omitempty"` + // Human readable message indicating details about last transition. + // +optional + Message string `json:"message,omitempty"` } +type RowConditionType string + +// These are valid conditions of a row. This list is not exhaustive and new conditions may be +// inculded by other resources. +const ( + // RowCompleted means the underlying resource has reached completion and may be given less + // visual priority than other resources. + RowCompleted RowConditionType = "Completed" +) + +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + // IncludeObjectPolicy controls which portion of the object is returned with a Table. type IncludeObjectPolicy string @@ -86,7 +135,7 @@ type TableOptions struct { // Specifying "None" will return no object, specifying "Object" will return the full object contents, and // specifying "Metadata" (the default) will return the object's metadata in the PartialObjectMetadata kind // in version v1alpha1 of the meta.k8s.io API group. - IncludeObject IncludeObjectPolicy `json:"includeObject,omitempty" protobuf:"bytes,1,opt,name=includeObject"` + IncludeObject IncludeObjectPolicy `json:"includeObject,omitempty" protobuf:"bytes,1,opt,name=includeObject,casttype=IncludeObjectPolicy"` } // PartialObjectMetadata is a generic representation of any object with ObjectMeta. It allows clients @@ -98,3 +147,11 @@ type PartialObjectMetadata struct { // +optional v1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` } + +// PartialObjectMetadataList contains a list of objects containing only their metadata +type PartialObjectMetadataList struct { + v1.TypeMeta `json:",inline"` + + // items contains each of the included items. + Items []*PartialObjectMetadata `json:"items" protobuf:"bytes,1,rep,name=items"` +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types_swagger_doc_generated.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types_swagger_doc_generated.go new file mode 100644 index 0000000000000..d2959b66508c3 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/types_swagger_doc_generated.go @@ -0,0 +1,104 @@ +/* +Copyright 2016 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 v1alpha1 + +// This file contains a collection of methods that can be used from go-restful to +// generate Swagger API documentation for its models. Please read this PR for more +// information on the implementation: https://github.com/emicklei/go-restful/pull/215 +// +// TODOs are ignored from the parser (e.g. TODO(andronat):... || TODO:...) if and only if +// they are on one line! For multiple line or blocks that you want to ignore use ---. +// Any context after a --- is ignored. +// +// Those methods can be generated by using hack/update-generated-swagger-docs.sh + +// AUTO-GENERATED FUNCTIONS START HERE +var map_PartialObjectMetadata = map[string]string{ + "": "PartialObjectMetadata is a generic representation of any object with ObjectMeta. It allows clients to get access to a particular ObjectMeta schema without knowing the details of the version.", + "metadata": "Standard object's metadata. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata", +} + +func (PartialObjectMetadata) SwaggerDoc() map[string]string { + return map_PartialObjectMetadata +} + +var map_PartialObjectMetadataList = map[string]string{ + "": "PartialObjectMetadataList contains a list of objects containing only their metadata", + "items": "items contains each of the included items.", +} + +func (PartialObjectMetadataList) SwaggerDoc() map[string]string { + return map_PartialObjectMetadataList +} + +var map_Table = map[string]string{ + "": "Table is a tabular representation of a set of API resources. The server transforms the object into a set of preferred columns for quickly reviewing the objects.", + "metadata": "Standard list metadata. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds", + "columnDefinitions": "columnDefinitions describes each column in the returned items array. The number of cells per row will always match the number of column definitions.", + "rows": "rows is the list of items in the table.", +} + +func (Table) SwaggerDoc() map[string]string { + return map_Table +} + +var map_TableColumnDefinition = map[string]string{ + "": "TableColumnDefinition contains information about a column returned in the Table.", + "name": "name is a human readable name for the column.", + "type": "type is an OpenAPI type definition for this column. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.", + "format": "format is an optional OpenAPI type definition for this column. The 'name' format is applied to the primary identifier column to assist in clients identifying column is the resource name. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.", + "description": "description is a human readable description of this column.", + "priority": "priority is an integer defining the relative importance of this column compared to others. Lower numbers are considered higher priority. Columns that may be omitted in limited space scenarios should be given a higher priority.", +} + +func (TableColumnDefinition) SwaggerDoc() map[string]string { + return map_TableColumnDefinition +} + +var map_TableOptions = map[string]string{ + "": "TableOptions are used when a Table is requested by the caller.", + "includeObject": "includeObject decides whether to include each object along with its columnar information. Specifying \"None\" will return no object, specifying \"Object\" will return the full object contents, and specifying \"Metadata\" (the default) will return the object's metadata in the PartialObjectMetadata kind in version v1alpha1 of the meta.k8s.io API group.", +} + +func (TableOptions) SwaggerDoc() map[string]string { + return map_TableOptions +} + +var map_TableRow = map[string]string{ + "": "TableRow is an individual row in a table.", + "cells": "cells will be as wide as headers and may contain strings, numbers, booleans, simple maps, or lists, or null. See the type field of the column definition for a more detailed description.", + "conditions": "conditions describe additional status of a row that are relevant for a human user.", + "object": "This field contains the requested additional information about each object based on the includeObject policy when requesting the Table. If \"None\", this field is empty, if \"Object\" this will be the default serialization of the object for the current API version, and if \"Metadata\" (the default) will contain the object metadata. Check the returned kind and apiVersion of the object before parsing.", +} + +func (TableRow) SwaggerDoc() map[string]string { + return map_TableRow +} + +var map_TableRowCondition = map[string]string{ + "": "TableRowCondition allows a row to be marked with additional information.", + "type": "Type of row condition.", + "status": "Status of the condition, one of True, False, Unknown.", + "reason": "(brief) machine readable reason for the condition's last transition.", + "message": "Human readable message indicating details about last transition.", +} + +func (TableRowCondition) SwaggerDoc() map[string]string { + return map_TableRowCondition +} + +// AUTO-GENERATED FUNCTIONS END HERE diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go index 367b11d96373b..ef8a117df81d3 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1alpha1/zz_generated.deepcopy.go @@ -31,10 +31,12 @@ import ( func GetGeneratedDeepCopyFuncs() []conversion.GeneratedDeepCopyFunc { return []conversion.GeneratedDeepCopyFunc{ {Fn: DeepCopy_v1alpha1_PartialObjectMetadata, InType: reflect.TypeOf(&PartialObjectMetadata{})}, + {Fn: DeepCopy_v1alpha1_PartialObjectMetadataList, InType: reflect.TypeOf(&PartialObjectMetadataList{})}, {Fn: DeepCopy_v1alpha1_Table, InType: reflect.TypeOf(&Table{})}, - {Fn: DeepCopy_v1alpha1_TableColumnDefinitions, InType: reflect.TypeOf(&TableColumnDefinitions{})}, + {Fn: DeepCopy_v1alpha1_TableColumnDefinition, InType: reflect.TypeOf(&TableColumnDefinition{})}, {Fn: DeepCopy_v1alpha1_TableOptions, InType: reflect.TypeOf(&TableOptions{})}, {Fn: DeepCopy_v1alpha1_TableRow, InType: reflect.TypeOf(&TableRow{})}, + {Fn: DeepCopy_v1alpha1_TableRowCondition, InType: reflect.TypeOf(&TableRowCondition{})}, } } @@ -53,6 +55,27 @@ func DeepCopy_v1alpha1_PartialObjectMetadata(in interface{}, out interface{}, c } } +// DeepCopy_v1alpha1_PartialObjectMetadataList is an autogenerated deepcopy function. +func DeepCopy_v1alpha1_PartialObjectMetadataList(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*PartialObjectMetadataList) + out := out.(*PartialObjectMetadataList) + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]*PartialObjectMetadata, len(*in)) + for i := range *in { + if newVal, err := c.DeepCopy(&(*in)[i]); err != nil { + return err + } else { + (*out)[i] = *newVal.(**PartialObjectMetadata) + } + } + } + return nil + } +} + // DeepCopy_v1alpha1_Table is an autogenerated deepcopy function. func DeepCopy_v1alpha1_Table(in interface{}, out interface{}, c *conversion.Cloner) error { { @@ -61,7 +84,7 @@ func DeepCopy_v1alpha1_Table(in interface{}, out interface{}, c *conversion.Clon *out = *in if in.ColumnDefinitions != nil { in, out := &in.ColumnDefinitions, &out.ColumnDefinitions - *out = make([]TableColumnDefinitions, len(*in)) + *out = make([]TableColumnDefinition, len(*in)) copy(*out, *in) } if in.Rows != nil { @@ -79,11 +102,11 @@ func DeepCopy_v1alpha1_Table(in interface{}, out interface{}, c *conversion.Clon } } -// DeepCopy_v1alpha1_TableColumnDefinitions is an autogenerated deepcopy function. -func DeepCopy_v1alpha1_TableColumnDefinitions(in interface{}, out interface{}, c *conversion.Cloner) error { +// DeepCopy_v1alpha1_TableColumnDefinition is an autogenerated deepcopy function. +func DeepCopy_v1alpha1_TableColumnDefinition(in interface{}, out interface{}, c *conversion.Cloner) error { { - in := in.(*TableColumnDefinitions) - out := out.(*TableColumnDefinitions) + in := in.(*TableColumnDefinition) + out := out.(*TableColumnDefinition) *out = *in return nil } @@ -116,6 +139,11 @@ func DeepCopy_v1alpha1_TableRow(in interface{}, out interface{}, c *conversion.C } } } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]TableRowCondition, len(*in)) + copy(*out, *in) + } if newVal, err := c.DeepCopy(&in.Object); err != nil { return err } else { @@ -124,3 +152,13 @@ func DeepCopy_v1alpha1_TableRow(in interface{}, out interface{}, c *conversion.C return nil } } + +// DeepCopy_v1alpha1_TableRowCondition is an autogenerated deepcopy function. +func DeepCopy_v1alpha1_TableRowCondition(in interface{}, out interface{}, c *conversion.Cloner) error { + { + in := in.(*TableRowCondition) + out := out.(*TableRowCondition) + *out = *in + return nil + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index 2bdc93a93adc1..53581c3419c77 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -28,12 +28,15 @@ go_test( "//vendor/k8s.io/apimachinery/pkg/api/testing: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/v1/unstructured: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/runtime/serializer:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/serializer/streaming:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go index 7cc02f7b279e5..24f0698f9ee40 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go @@ -43,11 +43,14 @@ import ( apitesting "k8s.io/apimachinery/pkg/api/testing" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" 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/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" @@ -754,6 +757,15 @@ func (storage *SimpleTypedStorage) checkContext(ctx request.Context) { storage.actualNamespace, storage.namespacePresent = request.NamespaceFrom(ctx) } +func bodyOrDie(response *http.Response) string { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + panic(err) + } + return string(body) +} + func extractBody(response *http.Response, object runtime.Object) (string, error) { return extractBodyDecoder(response, object, codec) } @@ -767,6 +779,16 @@ func extractBodyDecoder(response *http.Response, object runtime.Object, decoder return string(body), runtime.DecodeInto(decoder, body, object) } +func extractBodyObject(response *http.Response, decoder runtime.Decoder) (runtime.Object, string, error) { + defer response.Body.Close() + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, string(body), err + } + obj, err := runtime.Decode(decoder, body) + return obj, string(body), err +} + func TestNotFound(t *testing.T) { type T struct { Method string @@ -1531,6 +1553,222 @@ func TestGetPretty(t *testing.T) { } } +func TestGetTable(t *testing.T) { + now := metav1.Now() + storage := map[string]rest.Storage{} + obj := genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, + Other: "foo", + } + simpleStorage := SimpleRESTStorage{ + item: obj, + } + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id", + name: "id", + namespace: "default", + } + storage["simple"] = &simpleStorage + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + m, err := meta.Accessor(&obj) + if err != nil { + t.Fatal(err) + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + encodedBody, err := runtime.Encode(metainternalversion.Codecs.LegacyCodec(metav1alpha1.SchemeGroupVersion), partial) + if err != nil { + t.Fatal(err) + } + // the codec includes a trailing newline that is not present during decode + encodedBody = bytes.TrimSpace(encodedBody) + + metaDoc := metav1.ObjectMeta{}.SwaggerDoc() + + tests := []struct { + accept string + params url.Values + pretty bool + expected *metav1alpha1.Table + statusCode int + }{ + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=Table;v=v1alpha1;g=meta.k8s.io", + expected: &metav1alpha1.Table{ + TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1alpha1"}, + ColumnDefinitions: []metav1alpha1.TableColumnDefinition{ + {Name: "Namespace", Type: "string", Description: metaDoc["namespace"]}, + {Name: "Name", Type: "string", Description: metaDoc["name"]}, + {Name: "Created At", Type: "date", Description: metaDoc["creationTimestamp"]}, + }, + Rows: []metav1alpha1.TableRow{ + {Cells: []interface{}{"ns1", "foo1", now.Time.UTC().Format(time.RFC3339)}, Object: runtime.RawExtension{Raw: encodedBody}}, + }, + }, + }, + } + for i, test := range tests { + u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id") + if err != nil { + t.Fatal(err) + } + u.RawQuery = test.params.Encode() + req := &http.Request{Method: "GET", URL: u} + req.Header = http.Header{} + req.Header.Set("Accept", test.accept) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if test.statusCode != 0 { + if resp.StatusCode != test.statusCode { + t.Errorf("%d: unexpected response: %#v", resp) + } + continue + } + if resp.StatusCode != http.StatusOK { + t.Fatal(err) + } + var itemOut metav1alpha1.Table + if _, err = extractBody(resp, &itemOut); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.expected, &itemOut) { + t.Errorf("%d: did not match: %s", i, diff.ObjectReflectDiff(test.expected, &itemOut)) + } + } +} + +func TestGetPartialObjectMetadata(t *testing.T) { + now := metav1.Time{metav1.Now().Rfc3339Copy().Local()} + storage := map[string]rest.Storage{} + simpleStorage := SimpleRESTStorage{ + item: genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, + Other: "foo", + }, + list: []genericapitesting.Simple{ + { + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("newer")}, + Other: "foo", + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "ns2", CreationTimestamp: now, UID: types.UID("older")}, + Other: "bar", + }, + }, + } + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/id", + alternativeSet: sets.NewString("/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple"), + name: "id", + namespace: "default", + } + storage["simple"] = &simpleStorage + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + tests := []struct { + accept string + params url.Values + pretty bool + list bool + expected runtime.Object + expectKind schema.GroupVersionKind + statusCode int + }{ + { + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadata;v=v1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + list: true, + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadata;v=v1alpha1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadataList;v=v1alpha1;g=meta.k8s.io", + statusCode: http.StatusNotAcceptable, + }, + { + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadata;v=v1alpha1;g=meta.k8s.io", + expected: &metav1alpha1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("abcdef0123")}, + }, + expectKind: schema.GroupVersionKind{Kind: "PartialObjectMetadata", Group: "meta.k8s.io", Version: "v1alpha1"}, + }, + { + list: true, + accept: runtime.ContentTypeJSON + ";as=PartialObjectMetadataList;v=v1alpha1;g=meta.k8s.io", + expected: &metav1alpha1.PartialObjectMetadataList{ + Items: []*metav1alpha1.PartialObjectMetadata{ + { + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1alpha1", Kind: "PartialObjectMetadata"}, + ObjectMeta: metav1.ObjectMeta{Name: "foo1", Namespace: "ns1", CreationTimestamp: now, UID: types.UID("newer")}, + }, + { + TypeMeta: metav1.TypeMeta{APIVersion: "meta.k8s.io/v1alpha1", Kind: "PartialObjectMetadata"}, + ObjectMeta: metav1.ObjectMeta{Name: "foo2", Namespace: "ns2", CreationTimestamp: now, UID: types.UID("older")}, + }, + }, + }, + expectKind: schema.GroupVersionKind{Kind: "PartialObjectMetadataList", Group: "meta.k8s.io", Version: "v1alpha1"}, + }, + } + for i, test := range tests { + suffix := "/namespaces/default/simple/id" + if test.list { + suffix = "/namespaces/default/simple" + } + u, err := url.Parse(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + suffix) + if err != nil { + t.Fatal(err) + } + u.RawQuery = test.params.Encode() + req := &http.Request{Method: "GET", URL: u} + req.Header = http.Header{} + req.Header.Set("Accept", test.accept) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + if test.statusCode != 0 { + if resp.StatusCode != test.statusCode { + t.Errorf("%d: unexpected response: %#v", i, resp) + } + continue + } + if resp.StatusCode != http.StatusOK { + t.Errorf("%d: invalid status: %#v\n%s", i, resp, bodyOrDie(resp)) + continue + } + itemOut, body, err := extractBodyObject(resp, metainternalversion.Codecs.LegacyCodec(metav1alpha1.SchemeGroupVersion)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.expected, itemOut) { + t.Errorf("%d: did not match: %s", i, diff.ObjectReflectDiff(test.expected, itemOut)) + } + obj := &unstructured.Unstructured{} + if err := json.Unmarshal([]byte(body), obj); err != nil { + t.Fatal(err) + } + if obj.GetObjectKind().GroupVersionKind() != test.expectKind { + t.Errorf("%d: unexpected kind: %#v", i, obj.GetObjectKind().GroupVersionKind()) + } + } +} + func TestGetBinary(t *testing.T) { simpleStorage := SimpleRESTStorage{ stream: &SimpleStream{ @@ -2952,12 +3190,13 @@ func TestUpdateChecksDecode(t *testing.T) { } type setTestSelfLinker struct { - t *testing.T - expectedSet string - name string - namespace string - called bool - err error + t *testing.T + expectedSet string + alternativeSet sets.String + name string + namespace string + called bool + err error } func (s *setTestSelfLinker) Namespace(runtime.Object) (string, error) { return s.namespace, s.err } @@ -2965,7 +3204,9 @@ func (s *setTestSelfLinker) Name(runtime.Object) (string, error) { return s func (s *setTestSelfLinker) SelfLink(runtime.Object) (string, error) { return "", s.err } func (s *setTestSelfLinker) SetSelfLink(obj runtime.Object, selfLink string) error { if e, a := s.expectedSet, selfLink; e != a { - s.t.Errorf("expected '%v', got '%v'", e, a) + if !s.alternativeSet.Has(a) { + s.t.Errorf("expected '%v', got '%v'", e, a) + } } s.called = true return s.err diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index ae2b0a8b53744..15e8cd1187353 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -38,6 +38,7 @@ go_library( "namer.go", "patch.go", "proxy.go", + "response.go", "rest.go", "watch.go", ], @@ -50,6 +51,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/api/meta: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/conversion/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go index cd262706c265f..07bc8e280f471 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/errors.go @@ -29,6 +29,10 @@ type errNotAcceptable struct { accepted []string } +func NewNotAcceptableError(accepted []string) error { + return errNotAcceptable{accepted} +} + func (e errNotAcceptable) Error() string { return fmt.Sprintf("only the following media types are accepted: %v", strings.Join(e.accepted, ", ")) } @@ -47,6 +51,10 @@ type errUnsupportedMediaType struct { accepted []string } +func NewUnsupportedMediaTypeError(accepted []string) error { + return errUnsupportedMediaType{accepted} +} + func (e errUnsupportedMediaType) Error() string { return fmt.Sprintf("the body of the request was in an unknown format - accepted media types include: %v", strings.Join(e.accepted, ", ")) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go index c3948d4cde396..896961b6ba137 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation/negotiate.go @@ -40,27 +40,32 @@ func MediaTypesForSerializer(ns runtime.NegotiatedSerializer) (mediaTypes, strea return mediaTypes, streamMediaTypes } -func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) +func NegotiateOutputMediaType(req *http.Request, ns runtime.NegotiatedSerializer, restrictions EndpointRestrictions) (MediaTypeOptions, runtime.SerializerInfo, error) { + mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), restrictions) if !ok { supported, _ := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errNotAcceptable{supported} + return mediaType, runtime.SerializerInfo{}, NewNotAcceptableError(supported) } // TODO: move into resthandler - info := mediaType.accepted.Serializer - if (mediaType.pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { + info := mediaType.Accepted.Serializer + if (mediaType.Pretty || isPrettyPrint(req)) && info.PrettySerializer != nil { info.Serializer = info.PrettySerializer } - return info, nil + return mediaType, info, nil +} + +func NegotiateOutputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { + _, info, err := NegotiateOutputMediaType(req, ns, DefaultEndpointRestrictions) + return info, err } func NegotiateOutputStreamSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { - mediaType, ok := negotiateMediaTypeOptions(req.Header.Get("Accept"), acceptedMediaTypesForEndpoint(ns), defaultEndpointRestrictions) - if !ok || mediaType.accepted.Serializer.StreamSerializer == nil { + mediaType, ok := NegotiateMediaTypeOptions(req.Header.Get("Accept"), AcceptedMediaTypesForEndpoint(ns), DefaultEndpointRestrictions) + if !ok || mediaType.Accepted.Serializer.StreamSerializer == nil { _, supported := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errNotAcceptable{supported} + return runtime.SerializerInfo{}, NewNotAcceptableError(supported) } - return mediaType.accepted.Serializer, nil + return mediaType.Accepted.Serializer, nil } func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer) (runtime.SerializerInfo, error) { @@ -72,7 +77,7 @@ func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer mediaType, _, err := mime.ParseMediaType(mediaType) if err != nil { _, supported := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} + return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported) } for _, info := range mediaTypes { @@ -83,7 +88,7 @@ func NegotiateInputSerializer(req *http.Request, ns runtime.NegotiatedSerializer } _, supported := MediaTypesForSerializer(ns) - return runtime.SerializerInfo{}, errUnsupportedMediaType{supported} + return runtime.SerializerInfo{}, NewUnsupportedMediaTypeError(supported) } // isPrettyPrint returns true if the "pretty" query parameter is true or if the User-Agent @@ -131,9 +136,9 @@ func negotiate(header string, alternatives []string) (goautoneg.Accept, bool) { return goautoneg.Accept{}, false } -// endpointRestrictions is an interface that allows content-type negotiation +// EndpointRestrictions is an interface that allows content-type negotiation // to verify server support for specific options -type endpointRestrictions interface { +type EndpointRestrictions interface { // AllowsConversion should return true if the specified group version kind // is an allowed target object. AllowsConversion(schema.GroupVersionKind) bool @@ -145,7 +150,7 @@ type endpointRestrictions interface { AllowsStreamSchema(schema string) bool } -var defaultEndpointRestrictions = emptyEndpointRestrictions{} +var DefaultEndpointRestrictions = emptyEndpointRestrictions{} type emptyEndpointRestrictions struct{} @@ -153,9 +158,9 @@ func (emptyEndpointRestrictions) AllowsConversion(schema.GroupVersionKind) bool func (emptyEndpointRestrictions) AllowsServerVersion(string) bool { return false } func (emptyEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" } -// acceptedMediaType contains information about a valid media type that the +// AcceptedMediaType contains information about a valid media type that the // server can serialize. -type acceptedMediaType struct { +type AcceptedMediaType struct { // Type is the first part of the media type ("application") Type string // SubType is the second part of the media type ("json") @@ -164,40 +169,40 @@ type acceptedMediaType struct { Serializer runtime.SerializerInfo } -// mediaTypeOptions describes information for a given media type that may alter +// MediaTypeOptions describes information for a given media type that may alter // the server response -type mediaTypeOptions struct { +type MediaTypeOptions struct { // pretty is true if the requested representation should be formatted for human // viewing - pretty bool + Pretty bool // stream, if set, indicates that a streaming protocol variant of this encoding // is desired. The only currently supported value is watch which returns versioned // events. In the future, this may refer to other stream protocols. - stream string + Stream string // convert is a request to alter the type of object returned by the server from the // normal response - convert *schema.GroupVersionKind + Convert *schema.GroupVersionKind // useServerVersion is an optional version for the server group - useServerVersion string + UseServerVersion string // export is true if the representation requested should exclude fields the server // has set - export bool + Export bool // unrecognized is a list of all unrecognized keys - unrecognized []string + Unrecognized []string // the accepted media type from the client - accepted *acceptedMediaType + Accepted *AcceptedMediaType } // acceptMediaTypeOptions returns an options object that matches the provided media type params. If // it returns false, the provided options are not allowed and the media type must be skipped. These // parameters are unversioned and may not be changed. -func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) { - var options mediaTypeOptions +func acceptMediaTypeOptions(params map[string]string, accepts *AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) { + var options MediaTypeOptions // extract all known parameters for k, v := range params { @@ -205,66 +210,65 @@ func acceptMediaTypeOptions(params map[string]string, accepts *acceptedMediaType // controls transformation of the object when returned case "as": - if options.convert == nil { - options.convert = &schema.GroupVersionKind{} + if options.Convert == nil { + options.Convert = &schema.GroupVersionKind{} } - options.convert.Kind = v + options.Convert.Kind = v case "g": - if options.convert == nil { - options.convert = &schema.GroupVersionKind{} + if options.Convert == nil { + options.Convert = &schema.GroupVersionKind{} } - options.convert.Group = v + options.Convert.Group = v case "v": - if options.convert == nil { - options.convert = &schema.GroupVersionKind{} + if options.Convert == nil { + options.Convert = &schema.GroupVersionKind{} } - options.convert.Version = v + options.Convert.Version = v // controls the streaming schema case "stream": if len(v) > 0 && (accepts.Serializer.StreamSerializer == nil || !endpoint.AllowsStreamSchema(v)) { - return mediaTypeOptions{}, false + return MediaTypeOptions{}, false } - options.stream = v + options.Stream = v // controls the version of the server API group used // for generic output case "sv": if len(v) > 0 && !endpoint.AllowsServerVersion(v) { - return mediaTypeOptions{}, false + return MediaTypeOptions{}, false } - options.useServerVersion = v + options.UseServerVersion = v // if specified, the server should transform the returned // output and remove fields that are always server specified, // or which fit the default behavior. case "export": - options.export = v == "1" + options.Export = v == "1" // if specified, the pretty serializer will be used case "pretty": - options.pretty = v == "1" + options.Pretty = v == "1" default: - options.unrecognized = append(options.unrecognized, k) + options.Unrecognized = append(options.Unrecognized, k) } } - if options.convert != nil && !endpoint.AllowsConversion(*options.convert) { - return mediaTypeOptions{}, false + if options.Convert != nil && !endpoint.AllowsConversion(*options.Convert) { + return MediaTypeOptions{}, false } - options.accepted = accepts - + options.Accepted = accepts return options, true } -// negotiateMediaTypeOptions returns the most appropriate content type given the accept header and +// NegotiateMediaTypeOptions returns the most appropriate content type given the accept header and // a list of alternatives along with the accepted media type parameters. -func negotiateMediaTypeOptions(header string, accepted []acceptedMediaType, endpoint endpointRestrictions) (mediaTypeOptions, bool) { +func NegotiateMediaTypeOptions(header string, accepted []AcceptedMediaType, endpoint EndpointRestrictions) (MediaTypeOptions, bool) { if len(header) == 0 && len(accepted) > 0 { - return mediaTypeOptions{ - accepted: &accepted[0], + return MediaTypeOptions{ + Accepted: &accepted[0], }, true } @@ -282,19 +286,19 @@ func negotiateMediaTypeOptions(header string, accepted []acceptedMediaType, endp } } } - return mediaTypeOptions{}, false + return MediaTypeOptions{}, false } -// acceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which +// AcceptedMediaTypesForEndpoint returns an array of structs that are used to efficiently check which // allowed media types the server exposes. -func acceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []acceptedMediaType { - var acceptedMediaTypes []acceptedMediaType +func AcceptedMediaTypesForEndpoint(ns runtime.NegotiatedSerializer) []AcceptedMediaType { + var acceptedMediaTypes []AcceptedMediaType for _, info := range ns.SupportedMediaTypes() { segments := strings.SplitN(info.MediaType, "/", 2) if len(segments) == 1 { segments = append(segments, "*") } - t := acceptedMediaType{ + t := AcceptedMediaType{ Type: segments[0], SubType: segments[1], Serializer: info, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go new file mode 100644 index 0000000000000..1aec3ba2d2c08 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/response.go @@ -0,0 +1,195 @@ +/* +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 handlers + +import ( + "fmt" + "net/http" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + 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/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// 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: 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) + responsewriters.WriteRawJSON(int(status.Code), status, w) + return + } + + // If conversion was allowed by the scope, perform it before writing the response + if target := mediaType.Convert; target != nil { + switch { + + case target.Kind == "PartialObjectMetadata" && target.GroupVersion() == metav1alpha1.SchemeGroupVersion: + if meta.IsListType(result) { + // TODO: this should be calculated earlier + err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadata, but the requested object is a list (%T)", result)) + scope.err(err, w, req) + return + } + m, err := meta.Accessor(result) + if err != nil { + scope.err(err, w, req) + return + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + scope.err(err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1alpha1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, partial) + return + + case target.Kind == "PartialObjectMetadataList" && target.GroupVersion() == metav1alpha1.SchemeGroupVersion: + if !meta.IsListType(result) { + // TODO: this should be calculated earlier + err = newNotAcceptableError(fmt.Sprintf("you requested PartialObjectMetadataList, but the requested object is not a list (%T)", result)) + scope.err(err, w, req) + return + } + list := &metav1alpha1.PartialObjectMetadataList{} + err := meta.EachListItem(result, func(obj runtime.Object) error { + m, err := meta.Accessor(obj) + if err != nil { + return err + } + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + list.Items = append(list.Items, partial) + return nil + }) + if err != nil { + scope.err(err, w, req) + return + } + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + scope.err(err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1alpha1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, list) + return + + case target.Kind == "Table" && target.GroupVersion() == metav1alpha1.SchemeGroupVersion: + // TODO: relax the version abstraction + // TODO: skip if this is a status response (delete without body)? + + opts := &metav1alpha1.TableOptions{} + if err := metav1alpha1.ParameterCodec.DecodeParameters(req.URL.Query(), metav1alpha1.SchemeGroupVersion, opts); err != nil { + scope.err(err, w, req) + return + } + + table, err := scope.TableConvertor.ConvertToTable(ctx, result, opts) + if err != nil { + scope.err(err, w, req) + return + } + + for i := range table.Rows { + item := &table.Rows[i] + switch opts.IncludeObject { + case metav1alpha1.IncludeObject: + item.Object.Object, err = scope.Convertor.ConvertToVersion(item.Object.Object, scope.Kind.GroupVersion()) + if err != nil { + scope.err(err, w, req) + return + } + // TODO: rely on defaulting for the value here? + case metav1alpha1.IncludeMetadata, "": + m, err := meta.Accessor(item.Object.Object) + if err != nil { + scope.err(err, w, req) + return + } + // TODO: turn this into an internal type and do conversion in order to get object kind automatically set? + partial := meta.AsPartialObjectMetadata(m) + partial.GetObjectKind().SetGroupVersionKind(metav1alpha1.SchemeGroupVersion.WithKind("PartialObjectMetadata")) + item.Object.Object = partial + case metav1alpha1.IncludeNone: + item.Object.Object = nil + default: + // TODO: move this to validation on the table options? + err = errors.NewBadRequest(fmt.Sprintf("unrecognized includeObject value: %q", opts.IncludeObject)) + scope.err(err, w, req) + } + } + + // renegotiate under the internal version + _, info, err := negotiation.NegotiateOutputMediaType(req, metainternalversion.Codecs, &scope) + if err != nil { + scope.err(err, w, req) + return + } + encoder := metainternalversion.Codecs.EncoderForVersion(info.Serializer, metav1alpha1.SchemeGroupVersion) + responsewriters.SerializeObject(info.MediaType, encoder, w, req, statusCode, table) + return + + default: + // this block should only be hit if scope AllowsConversion is incorrect + accepted, _ := negotiation.MediaTypesForSerializer(metainternalversion.Codecs) + err := negotiation.NewNotAcceptableError(accepted) + status := responsewriters.ErrorToAPIStatus(err) + responsewriters.WriteRawJSON(int(status.Code), status, w) + return + } + } + + responsewriters.WriteObject(ctx, statusCode, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) +} + +// errNotAcceptable indicates Accept negotiation has failed +type errNotAcceptable struct { + message string +} + +func newNotAcceptableError(message string) error { + return errNotAcceptable{message} +} + +func (e errNotAcceptable) Error() string { + return e.message +} + +func (e errNotAcceptable) Status() metav1.Status { + return metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotAcceptable, + Reason: metav1.StatusReason("NotAcceptable"), + Message: e.Error(), + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go index 92172ad7e19a6..2f67cb6f9d629 100755 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status.go @@ -30,8 +30,8 @@ type statusError interface { Status() metav1.Status } -// apiStatus converts an error to an metav1.Status object. -func apiStatus(err error) *metav1.Status { +// ErrorToAPIStatus converts an error to an metav1.Status object. +func ErrorToAPIStatus(err error) *metav1.Status { switch t := err.(type) { case statusError: status := t.Status() diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go index 2422e76c143cc..60168e24dbc56 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/status_test.go @@ -64,7 +64,7 @@ func TestAPIStatus(t *testing.T) { }, } for k, v := range cases { - actual := apiStatus(k) + actual := ErrorToAPIStatus(k) if !reflect.DeepEqual(actual, &v) { t.Errorf("%s: Expected %#v, Got %#v", k, v, actual) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go index 1e88557531f71..32ea7bd6cd53a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters/writers.go @@ -41,11 +41,17 @@ import ( // be "application/octet-stream". All other objects are sent to standard JSON serialization. func WriteObject(ctx request.Context, statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, object runtime.Object, w http.ResponseWriter, req *http.Request) { stream, ok := object.(rest.ResourceStreamer) - if !ok { - WriteObjectNegotiated(ctx, s, gv, w, req, statusCode, object) + if ok { + StreamObject(ctx, statusCode, gv, s, stream, w, req) return } + WriteObjectNegotiated(ctx, s, gv, w, req, statusCode, object) +} +// StreamObject performs input stream negotiation from a ResourceStreamer and writes that to the response. +// If the client requests a websocket upgrade, negotiate for a websocket reader protocol (because many +// browser clients cannot easily handle binary streaming protocols). +func StreamObject(ctx request.Context, statusCode int, gv schema.GroupVersion, s runtime.NegotiatedSerializer, stream rest.ResourceStreamer, w http.ResponseWriter, req *http.Request) { out, flush, contentType, err := stream.InputStream(gv.String(), req.Header.Get("Accept")) if err != nil { ErrorNegotiated(ctx, err, s, gv, w, req) @@ -78,12 +84,23 @@ func WriteObject(ctx request.Context, statusCode int, gv schema.GroupVersion, s io.Copy(writer, out) } +// SerializeObject renders an object in the content type negotiated by the client using the provided encoder. +// The context is optional and can be nil. +func SerializeObject(mediaType string, encoder runtime.Encoder, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { + w.Header().Set("Content-Type", mediaType) + w.WriteHeader(statusCode) + + if err := encoder.Encode(object, w); err != nil { + errorJSONFatal(err, encoder, w) + } +} + // WriteObjectNegotiated renders an object in the content type negotiated by the client. // The context is optional and can be nil. func WriteObjectNegotiated(ctx request.Context, s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request, statusCode int, object runtime.Object) { serializer, err := negotiation.NegotiateOutputSerializer(req, s) if err != nil { - status := apiStatus(err) + status := ErrorToAPIStatus(err) WriteRawJSON(int(status.Code), status, w) return } @@ -96,15 +113,13 @@ func WriteObjectNegotiated(ctx request.Context, s runtime.NegotiatedSerializer, w.WriteHeader(statusCode) encoder := s.EncoderForVersion(serializer.Serializer, gv) - if err := encoder.Encode(object, w); err != nil { - errorJSONFatal(err, encoder, w) - } + SerializeObject(serializer.MediaType, encoder, w, req, statusCode, object) } // ErrorNegotiated renders an error to the response. Returns the HTTP status code of the error. -// The context is options and may be nil. +// The context is optional and may be nil. func ErrorNegotiated(ctx request.Context, err error, s runtime.NegotiatedSerializer, gv schema.GroupVersion, w http.ResponseWriter, req *http.Request) int { - status := apiStatus(err) + status := ErrorToAPIStatus(err) code := int(status.Code) // when writing an error, check to see if the status indicates a retry after period if status.Details != nil && status.Details.RetryAfterSeconds > 0 { @@ -125,7 +140,7 @@ func ErrorNegotiated(ctx request.Context, err error, s runtime.NegotiatedSeriali // Returns the HTTP status code of the error. func errorJSONFatal(err error, codec runtime.Encoder, w http.ResponseWriter) int { utilruntime.HandleError(fmt.Errorf("apiserver was unable to write a JSON response: %v", err)) - status := apiStatus(err) + status := ErrorToAPIStatus(err) code := int(status.Code) output, err := runtime.Encode(codec, status) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 854091c855393..578d1c95ff225 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -32,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" 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/conversion/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -65,6 +66,8 @@ type RequestScope struct { Typer runtime.ObjectTyper UnsafeConvertor runtime.ObjectConvertor + TableConvertor rest.TableConvertor + Resource schema.GroupVersionResource Kind schema.GroupVersionKind Subresource string @@ -77,6 +80,30 @@ func (scope *RequestScope) err(err error, w http.ResponseWriter, req *http.Reque responsewriters.ErrorNegotiated(ctx, err, scope.Serializer, scope.Kind.GroupVersion(), w, req) } +func (scope *RequestScope) AllowsConversion(gvk schema.GroupVersionKind) bool { + // TODO: this is temporary, replace with an abstraction calculated at endpoint installation time + if gvk.GroupVersion() == metav1alpha1.SchemeGroupVersion { + switch gvk.Kind { + case "Table": + return scope.TableConvertor != nil + case "PartialObjectMetadata", "PartialObjectMetadataList": + // TODO: should delineate between lists and non-list endpoints + return true + default: + return false + } + } + return false +} + +func (scope *RequestScope) AllowsServerVersion(version string) bool { + return version == scope.MetaGroupVersion.Version +} + +func (scope *RequestScope) AllowsStreamSchema(s string) bool { + return s == "watch" +} + // getterFunc performs a get request with the given context and object name. The request // may be used to deserialize an options object to pass to the getter. type getterFunc func(ctx request.Context, name string, req *http.Request, trace *utiltrace.Trace) (runtime.Object, error) @@ -115,7 +142,7 @@ func getResourceHandler(scope RequestScope, getter getterFunc) http.HandlerFunc } trace.Step("About to write a response") - responsewriters.WriteObject(ctx, http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) } } @@ -348,7 +375,7 @@ func ListResource(r rest.Lister, rw rest.Watcher, scope RequestScope, forceWatch } } - responsewriters.WriteObject(ctx, http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) trace.Step(fmt.Sprintf("Writing http response done (%d items)", numberOfItems)) } } @@ -447,7 +474,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object } trace.Step("Self-link added") - responsewriters.WriteObject(ctx, http.StatusCreated, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusCreated, result) } } @@ -547,9 +574,8 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface return } - responsewriters.WriteObject(ctx, http.StatusOK, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) } - } type updateAdmissionFunc func(updatedObject runtime.Object, currentObject runtime.Object) error @@ -877,7 +903,8 @@ func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectType if wasCreated { status = http.StatusCreated } - responsewriters.WriteObject(ctx, status, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + + transformResponseObject(ctx, scope, req, w, status, result) } } @@ -996,7 +1023,7 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope RequestSco } } - responsewriters.WriteObject(ctx, status, scope.Kind.GroupVersion(), scope.Serializer, result, w, req) + transformResponseObject(ctx, scope, req, w, status, result) } } @@ -1102,7 +1129,7 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope RequestSco } } - responsewriters.WriteObjectNegotiated(ctx, scope.Serializer, scope.Kind.GroupVersion(), w, req, http.StatusOK, result) + transformResponseObject(ctx, scope, req, w, http.StatusOK, result) } } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index f4e66bd6ad857..eef3b64c62f09 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -26,6 +26,8 @@ import ( "time" "unicode" + restful "github.com/emicklei/go-restful" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" @@ -38,8 +40,6 @@ import ( "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" - - "github.com/emicklei/go-restful" ) const ( @@ -374,6 +374,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag shortNames = shortNamesProvider.ShortNames() } + tableProvider, ok := storage.(rest.TableConvertor) + if !ok { + tableProvider = rest.DefaultTableConvertor + } + var apiResource metav1.APIResource // Get the list of actions for the given scope. switch scope.Name() { @@ -525,6 +530,9 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Typer: a.group.Typer, UnsafeConvertor: a.group.UnsafeConvertor, + // TODO: Check for the interface on storage + TableConvertor: tableProvider, + // TODO: This seems wrong for cross-group subresources. It makes an assumption that a subresource and its parent are in the same group version. Revisit this. Resource: a.group.GroupVersion.WithResource(resource), Subresource: subresource, 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 32697b60893e8..15a507c6638ae 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 74b0fa6b7c7a2..74ddaee2c674b 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" @@ -154,6 +155,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 @@ -168,6 +172,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" @@ -1242,3 +1247,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 3911586ccdf53..7dcca50ca6281 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 cc6613f9d6c66..015754f8602f7 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go @@ -23,6 +23,7 @@ import ( 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/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" @@ -116,6 +117,10 @@ type GetterWithOptions interface { NewGetOptions() (runtime.Object, bool, string) } +type TableConvertor interface { + ConvertToTable(ctx genericapirequest.Context, object runtime.Object, tableOptions runtime.Object) (*metav1alpha1.Table, error) +} + // Deleter is an object that can delete a named RESTful resource. type Deleter interface { // Delete finds a resource in the storage and deletes it. diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go new file mode 100644 index 0000000000000..cc1e83d2bdf29 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 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 rest + +import ( + "time" + + "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/runtime" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" +) + +var DefaultTableConvertor TableConvertor = defaultTableConvertor{} + +type defaultTableConvertor struct{} + +var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc() + +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.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}, + }) + return nil + } + switch { + case meta.IsListType(object): + if err := meta.EachListItem(object, fn); err != nil { + return nil, err + } + default: + if err := fn(object); err != nil { + return nil, err + } + } + 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"]}, + {Name: "Created At", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"]}, + } + // trim the left two columns if completely empty + if trimColumn(0, &table) { + trimColumn(0, &table) + } else { + trimColumn(1, &table) + } + return &table, nil +} + +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 { + return false + } + case interface{}: + if t == nil { + return false + } + } + } + if column == 0 { + table.ColumnDefinitions = table.ColumnDefinitions[1:] + } else { + for j := column; j < len(table.ColumnDefinitions); j++ { + table.ColumnDefinitions[j] = table.ColumnDefinitions[j+1] + } + } + for i := range table.Rows { + cells := table.Rows[i].Cells + if column == 0 { + table.Rows[i].Cells = cells[1:] + continue + } + for j := column; j < len(cells); j++ { + cells[j] = cells[j+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 bc4dcc53c1233..d6b84408d6a75 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"