123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- /*
- Copyright 2019 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package printers
- import (
- "fmt"
- "io"
- "reflect"
- "strings"
- "time"
- "github.com/liggitt/tabwriter"
- "k8s.io/apimachinery/pkg/api/meta"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/util/duration"
- )
- var _ ResourcePrinter = &HumanReadablePrinter{}
- var (
- defaultHandlerEntry = &handlerEntry{
- columnDefinitions: objectMetaColumnDefinitions,
- printFunc: reflect.ValueOf(printObjectMeta),
- }
- objectMetaColumnDefinitions = []metav1beta1.TableColumnDefinition{
- {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
- {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
- }
- withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
- )
- // NewTablePrinter creates a printer suitable for calling PrintObj().
- // TODO(seans3): Change return type to ResourcePrinter interface once we no longer need
- // to constuct the "handlerMap".
- func NewTablePrinter(options PrintOptions) *HumanReadablePrinter {
- printer := &HumanReadablePrinter{
- handlerMap: make(map[reflect.Type]*handlerEntry),
- options: options,
- }
- return printer
- }
- func printHeader(columnNames []string, w io.Writer) error {
- if _, err := fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil {
- return err
- }
- return nil
- }
- // 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 {
- w, found := output.(*tabwriter.Writer)
- if !found {
- w = GetNewTabWriter(output)
- output = w
- defer w.Flush()
- }
- // Case 1: Parameter "obj" is a table from server; print it.
- // display tables following the rules of options
- if table, ok := obj.(*metav1beta1.Table); ok {
- // Do not print headers if this table has no column definitions, or they are the same as the last ones we printed
- localOptions := h.options
- if len(table.ColumnDefinitions) == 0 || reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns) {
- localOptions.NoHeaders = true
- }
- if len(table.ColumnDefinitions) == 0 {
- // If this table has no column definitions, use the columns from the last table we printed for decoration and layout.
- // This is done when receiving tables in watch events to save bandwidth.
- localOptions.NoHeaders = true
- table.ColumnDefinitions = h.lastColumns
- } else {
- // If this table has column definitions, remember them for future use.
- h.lastColumns = table.ColumnDefinitions
- }
- if err := decorateTable(table, localOptions); err != nil {
- return err
- }
- return printTable(table, output, localOptions)
- }
- // Case 2: Parameter "obj" is not a table; search for a handler to print it.
- // TODO(seans3): Remove this case in 1.16, since table should be returned from server-side printing.
- // print with a registered handler
- t := reflect.TypeOf(obj)
- if handler := h.handlerMap[t]; handler != nil {
- includeHeaders := h.lastType != t && !h.options.NoHeaders
- if h.lastType != nil && h.lastType != t && !h.options.NoHeaders {
- fmt.Fprintln(output)
- }
- if err := printRowsForHandlerEntry(output, handler, obj, h.options, includeHeaders); err != nil {
- return err
- }
- h.lastType = t
- return nil
- }
- // Case 3: Could not find print handler for "obj"; use the default print handler.
- // Print with the default handler, and use the columns from the last time
- includeHeaders := h.lastType != defaultHandlerEntry && !h.options.NoHeaders
- if h.lastType != nil && h.lastType != defaultHandlerEntry && !h.options.NoHeaders {
- fmt.Fprintln(output)
- }
- if err := printRowsForHandlerEntry(output, defaultHandlerEntry, obj, h.options, includeHeaders); err != nil {
- return err
- }
- h.lastType = defaultHandlerEntry
- return nil
- }
- // printTable prints a table to the provided output respecting the filtering rules for options
- // for wide columns and filtered 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 *metav1beta1.Table, output io.Writer, options PrintOptions) error {
- if !options.NoHeaders {
- // avoid printing headers if we have no rows to display
- if len(table.Rows) == 0 {
- return nil
- }
- 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 {
- first := true
- for i, cell := range row.Cells {
- if i >= len(table.ColumnDefinitions) {
- // https://issue.k8s.io/66379
- // don't panic in case of bad output from the server, with more cells than column definitions
- break
- }
- 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 *metav1beta1.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 && !options.Kind.Empty() {
- for i := range columns {
- if columns[i].Format == "name" && columns[i].Type == "string" {
- nameColumn = i
- break
- }
- }
- }
- if width != len(table.ColumnDefinitions) {
- columns = make([]metav1beta1.TableColumnDefinition, 0, width)
- if options.WithNamespace {
- columns = append(columns, metav1beta1.TableColumnDefinition{
- Name: "Namespace",
- Type: "string",
- })
- }
- columns = append(columns, table.ColumnDefinitions...)
- for _, label := range formatLabelHeaders(options.ColumnLabels) {
- columns = append(columns, metav1beta1.TableColumnDefinition{
- Name: label,
- Type: "string",
- })
- }
- if options.ShowLabels {
- columns = append(columns, metav1beta1.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", strings.ToLower(options.Kind.String()), 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
- }
- // printRowsForHandlerEntry prints the incremental table output (headers if the current type is
- // different from lastType) including all the rows in the object. It returns the current type
- // or an error, if any.
- func printRowsForHandlerEntry(output io.Writer, handler *handlerEntry, obj runtime.Object, options PrintOptions, includeHeaders bool) error {
- var results []reflect.Value
- args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
- results = handler.printFunc.Call(args)
- if !results[1].IsNil() {
- return results[1].Interface().(error)
- }
- if includeHeaders {
- var headers []string
- for _, column := range handler.columnDefinitions {
- if column.Priority != 0 && !options.Wide {
- continue
- }
- headers = append(headers, strings.ToUpper(column.Name))
- }
- headers = append(headers, formatLabelHeaders(options.ColumnLabels)...)
- // LABELS is always the last column.
- headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...)
- if options.WithNamespace {
- headers = append(withNamespacePrefixColumns, headers...)
- }
- printHeader(headers, output)
- }
- if results[1].IsNil() {
- rows := results[0].Interface().([]metav1beta1.TableRow)
- printRows(output, rows, options)
- return nil
- }
- return results[1].Interface().(error)
- }
- // printRows writes the provided rows to output.
- func printRows(output io.Writer, rows []metav1beta1.TableRow, options PrintOptions) {
- for _, row := range rows {
- if options.WithNamespace {
- if obj := row.Object.Object; obj != nil {
- if m, err := meta.Accessor(obj); err == nil {
- fmt.Fprint(output, m.GetNamespace())
- }
- }
- fmt.Fprint(output, "\t")
- }
- for i, cell := range row.Cells {
- if i != 0 {
- fmt.Fprint(output, "\t")
- } else {
- // TODO: remove this once we drop the legacy printers
- if options.WithKind && !options.Kind.Empty() {
- fmt.Fprintf(output, "%s/%s", strings.ToLower(options.Kind.String()), cell)
- continue
- }
- }
- fmt.Fprint(output, cell)
- }
- hasLabels := len(options.ColumnLabels) > 0
- if obj := row.Object.Object; obj != nil && (hasLabels || options.ShowLabels) {
- if m, err := meta.Accessor(obj); err == nil {
- for _, value := range labelValues(m.GetLabels(), options) {
- output.Write([]byte("\t"))
- output.Write([]byte(value))
- }
- }
- }
- output.Write([]byte("\n"))
- }
- }
- func formatLabelHeaders(columnLabels []string) []string {
- formHead := make([]string, len(columnLabels))
- for i, l := range columnLabels {
- p := strings.Split(l, "/")
- formHead[i] = strings.ToUpper((p[len(p)-1]))
- }
- return formHead
- }
- // headers for --show-labels=true
- func formatShowLabelsHeader(showLabels bool) []string {
- if showLabels {
- return []string{"LABELS"}
- }
- 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
- }
- func printObjectMeta(obj runtime.Object, options PrintOptions) ([]metav1beta1.TableRow, error) {
- if meta.IsListType(obj) {
- rows := make([]metav1beta1.TableRow, 0, 16)
- err := meta.EachListItem(obj, func(obj runtime.Object) error {
- nestedRows, err := printObjectMeta(obj, options)
- if err != nil {
- return err
- }
- rows = append(rows, nestedRows...)
- return nil
- })
- if err != nil {
- return nil, err
- }
- return rows, nil
- }
- rows := make([]metav1beta1.TableRow, 0, 1)
- m, err := meta.Accessor(obj)
- if err != nil {
- return nil, err
- }
- row := metav1beta1.TableRow{
- Object: runtime.RawExtension{Object: obj},
- }
- row.Cells = append(row.Cells, m.GetName(), translateTimestampSince(m.GetCreationTimestamp()))
- rows = append(rows, row)
- return rows, nil
- }
- // translateTimestampSince returns the elapsed time since timestamp in
- // human-readable approximation.
- func translateTimestampSince(timestamp metav1.Time) string {
- if timestamp.IsZero() {
- return "<unknown>"
- }
- return duration.HumanDuration(time.Since(timestamp.Time))
- }
|