...

Source file src/k8s.io/cli-runtime/pkg/printers/tableprinter.go

Documentation: k8s.io/cli-runtime/pkg/printers

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package printers
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"reflect"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/liggitt/tabwriter"
    27  	"k8s.io/apimachinery/pkg/api/meta"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/duration"
    32  	"k8s.io/apimachinery/pkg/watch"
    33  )
    34  
    35  var _ ResourcePrinter = &HumanReadablePrinter{}
    36  
    37  type printHandler struct {
    38  	columnDefinitions []metav1.TableColumnDefinition
    39  	printFunc         reflect.Value
    40  }
    41  
    42  var (
    43  	statusHandlerEntry = &printHandler{
    44  		columnDefinitions: statusColumnDefinitions,
    45  		printFunc:         reflect.ValueOf(printStatus),
    46  	}
    47  
    48  	statusColumnDefinitions = []metav1.TableColumnDefinition{
    49  		{Name: "Status", Type: "string"},
    50  		{Name: "Reason", Type: "string"},
    51  		{Name: "Message", Type: "string"},
    52  	}
    53  
    54  	defaultHandlerEntry = &printHandler{
    55  		columnDefinitions: objectMetaColumnDefinitions,
    56  		printFunc:         reflect.ValueOf(printObjectMeta),
    57  	}
    58  
    59  	objectMetaColumnDefinitions = []metav1.TableColumnDefinition{
    60  		{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
    61  		{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
    62  	}
    63  
    64  	withEventTypePrefixColumns = []string{"EVENT"}
    65  	withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
    66  )
    67  
    68  // HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide
    69  // more elegant output. It is not threadsafe, but you may call PrintObj repeatedly; headers
    70  // will only be printed if the object type changes. This makes it useful for printing items
    71  // received from watches.
    72  type HumanReadablePrinter struct {
    73  	options        PrintOptions
    74  	lastType       interface{}
    75  	lastColumns    []metav1.TableColumnDefinition
    76  	printedHeaders bool
    77  }
    78  
    79  // NewTablePrinter creates a printer suitable for calling PrintObj().
    80  func NewTablePrinter(options PrintOptions) ResourcePrinter {
    81  	printer := &HumanReadablePrinter{
    82  		options: options,
    83  	}
    84  	return printer
    85  }
    86  
    87  func printHeader(columnNames []string, w io.Writer) error {
    88  	if _, err := fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil {
    89  		return err
    90  	}
    91  	return nil
    92  }
    93  
    94  // PrintObj prints the obj in a human-friendly format according to the type of the obj.
    95  func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error {
    96  
    97  	if _, found := output.(*tabwriter.Writer); !found {
    98  		w := GetNewTabWriter(output)
    99  		output = w
   100  		defer w.Flush()
   101  	}
   102  
   103  	var eventType string
   104  	if event, isEvent := obj.(*metav1.WatchEvent); isEvent {
   105  		eventType = event.Type
   106  		obj = event.Object.Object
   107  	}
   108  
   109  	// Parameter "obj" is a table from server; print it.
   110  	// display tables following the rules of options
   111  	if table, ok := obj.(*metav1.Table); ok {
   112  		// Do not print headers if this table has no column definitions, or they are the same as the last ones we printed
   113  		localOptions := h.options
   114  		if h.printedHeaders && (len(table.ColumnDefinitions) == 0 || reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns)) {
   115  			localOptions.NoHeaders = true
   116  		}
   117  
   118  		if len(table.ColumnDefinitions) == 0 {
   119  			// If this table has no column definitions, use the columns from the last table we printed for decoration and layout.
   120  			// This is done when receiving tables in watch events to save bandwidth.
   121  			table.ColumnDefinitions = h.lastColumns
   122  		} else if !reflect.DeepEqual(table.ColumnDefinitions, h.lastColumns) {
   123  			// If this table has column definitions, remember them for future use.
   124  			h.lastColumns = table.ColumnDefinitions
   125  			h.printedHeaders = false
   126  		}
   127  
   128  		if len(table.Rows) > 0 {
   129  			h.printedHeaders = true
   130  		}
   131  
   132  		if err := decorateTable(table, localOptions); err != nil {
   133  			return err
   134  		}
   135  		if len(eventType) > 0 {
   136  			if err := addColumns(beginning, table,
   137  				[]metav1.TableColumnDefinition{{Name: "Event", Type: "string"}},
   138  				[]cellValueFunc{func(metav1.TableRow) (interface{}, error) { return formatEventType(eventType), nil }},
   139  			); err != nil {
   140  				return err
   141  			}
   142  		}
   143  		return printTable(table, output, localOptions)
   144  	}
   145  
   146  	// Could not find print handler for "obj"; use the default or status print handler.
   147  	// Print with the default or status handler, and use the columns from the last time
   148  	var handler *printHandler
   149  	if _, isStatus := obj.(*metav1.Status); isStatus {
   150  		handler = statusHandlerEntry
   151  	} else {
   152  		handler = defaultHandlerEntry
   153  	}
   154  
   155  	includeHeaders := h.lastType != handler && !h.options.NoHeaders
   156  
   157  	if h.lastType != nil && h.lastType != handler && !h.options.NoHeaders {
   158  		fmt.Fprintln(output)
   159  	}
   160  
   161  	if err := printRowsForHandlerEntry(output, handler, eventType, obj, h.options, includeHeaders); err != nil {
   162  		return err
   163  	}
   164  	h.lastType = handler
   165  
   166  	return nil
   167  }
   168  
   169  // printTable prints a table to the provided output respecting the filtering rules for options
   170  // for wide columns and filtered rows. It filters out rows that are Completed. You should call
   171  // decorateTable if you receive a table from a remote server before calling printTable.
   172  func printTable(table *metav1.Table, output io.Writer, options PrintOptions) error {
   173  	if !options.NoHeaders {
   174  		// avoid printing headers if we have no rows to display
   175  		if len(table.Rows) == 0 {
   176  			return nil
   177  		}
   178  
   179  		first := true
   180  		for _, column := range table.ColumnDefinitions {
   181  			if !options.Wide && column.Priority != 0 {
   182  				continue
   183  			}
   184  			if first {
   185  				first = false
   186  			} else {
   187  				fmt.Fprint(output, "\t")
   188  			}
   189  			fmt.Fprint(output, strings.ToUpper(column.Name))
   190  		}
   191  		fmt.Fprintln(output)
   192  	}
   193  	for _, row := range table.Rows {
   194  		first := true
   195  		for i, cell := range row.Cells {
   196  			if i >= len(table.ColumnDefinitions) {
   197  				// https://issue.k8s.io/66379
   198  				// don't panic in case of bad output from the server, with more cells than column definitions
   199  				break
   200  			}
   201  			column := table.ColumnDefinitions[i]
   202  			if !options.Wide && column.Priority != 0 {
   203  				continue
   204  			}
   205  			if first {
   206  				first = false
   207  			} else {
   208  				fmt.Fprint(output, "\t")
   209  			}
   210  			if cell != nil {
   211  				switch val := cell.(type) {
   212  				case string:
   213  					print := val
   214  					truncated := false
   215  					// Truncate at the first newline, carriage return or formfeed
   216  					// (treated as a newline by tabwriter).
   217  					breakchar := strings.IndexAny(print, "\f\n\r")
   218  					if breakchar >= 0 {
   219  						truncated = true
   220  						print = print[:breakchar]
   221  					}
   222  					WriteEscaped(output, print)
   223  					if truncated {
   224  						fmt.Fprint(output, "...")
   225  					}
   226  				default:
   227  					WriteEscaped(output, fmt.Sprint(val))
   228  				}
   229  			}
   230  		}
   231  		fmt.Fprintln(output)
   232  	}
   233  	return nil
   234  }
   235  
   236  type cellValueFunc func(metav1.TableRow) (interface{}, error)
   237  
   238  type columnAddPosition int
   239  
   240  const (
   241  	beginning columnAddPosition = 1
   242  	end       columnAddPosition = 2
   243  )
   244  
   245  func addColumns(pos columnAddPosition, table *metav1.Table, columns []metav1.TableColumnDefinition, valueFuncs []cellValueFunc) error {
   246  	if len(columns) != len(valueFuncs) {
   247  		return fmt.Errorf("cannot prepend columns, unmatched value functions")
   248  	}
   249  	if len(columns) == 0 {
   250  		return nil
   251  	}
   252  
   253  	// Compute the new rows
   254  	newRows := make([][]interface{}, len(table.Rows))
   255  	for i := range table.Rows {
   256  		newCells := make([]interface{}, 0, len(columns)+len(table.Rows[i].Cells))
   257  
   258  		if pos == end {
   259  			// If we're appending, start with the existing cells,
   260  			// then add nil cells to match the number of columns
   261  			newCells = append(newCells, table.Rows[i].Cells...)
   262  			for len(newCells) < len(table.ColumnDefinitions) {
   263  				newCells = append(newCells, nil)
   264  			}
   265  		}
   266  
   267  		// Compute cells for new columns
   268  		for _, f := range valueFuncs {
   269  			newCell, err := f(table.Rows[i])
   270  			if err != nil {
   271  				return err
   272  			}
   273  			newCells = append(newCells, newCell)
   274  		}
   275  
   276  		if pos == beginning {
   277  			// If we're prepending, add existing cells
   278  			newCells = append(newCells, table.Rows[i].Cells...)
   279  		}
   280  
   281  		// Remember the new cells for this row
   282  		newRows[i] = newCells
   283  	}
   284  
   285  	// All cells successfully computed, now replace columns and rows
   286  	newColumns := make([]metav1.TableColumnDefinition, 0, len(columns)+len(table.ColumnDefinitions))
   287  	switch pos {
   288  	case beginning:
   289  		newColumns = append(newColumns, columns...)
   290  		newColumns = append(newColumns, table.ColumnDefinitions...)
   291  	case end:
   292  		newColumns = append(newColumns, table.ColumnDefinitions...)
   293  		newColumns = append(newColumns, columns...)
   294  	default:
   295  		return fmt.Errorf("invalid column add position: %v", pos)
   296  	}
   297  	table.ColumnDefinitions = newColumns
   298  	for i := range table.Rows {
   299  		table.Rows[i].Cells = newRows[i]
   300  	}
   301  
   302  	return nil
   303  }
   304  
   305  // decorateTable takes a table and attempts to add label columns and the
   306  // namespace column. It will fill empty columns with nil (if the object
   307  // does not expose metadata). It returns an error if the table cannot
   308  // be decorated.
   309  func decorateTable(table *metav1.Table, options PrintOptions) error {
   310  	width := len(table.ColumnDefinitions) + len(options.ColumnLabels)
   311  	if options.WithNamespace {
   312  		width++
   313  	}
   314  	if options.ShowLabels {
   315  		width++
   316  	}
   317  
   318  	columns := table.ColumnDefinitions
   319  
   320  	nameColumn := -1
   321  	if options.WithKind && !options.Kind.Empty() {
   322  		for i := range columns {
   323  			if columns[i].Format == "name" && columns[i].Type == "string" {
   324  				nameColumn = i
   325  				break
   326  			}
   327  		}
   328  	}
   329  
   330  	if width != len(table.ColumnDefinitions) {
   331  		columns = make([]metav1.TableColumnDefinition, 0, width)
   332  		if options.WithNamespace {
   333  			columns = append(columns, metav1.TableColumnDefinition{
   334  				Name: "Namespace",
   335  				Type: "string",
   336  			})
   337  		}
   338  		columns = append(columns, table.ColumnDefinitions...)
   339  		for _, label := range formatLabelHeaders(options.ColumnLabels) {
   340  			columns = append(columns, metav1.TableColumnDefinition{
   341  				Name: label,
   342  				Type: "string",
   343  			})
   344  		}
   345  		if options.ShowLabels {
   346  			columns = append(columns, metav1.TableColumnDefinition{
   347  				Name: "Labels",
   348  				Type: "string",
   349  			})
   350  		}
   351  	}
   352  
   353  	rows := table.Rows
   354  
   355  	includeLabels := len(options.ColumnLabels) > 0 || options.ShowLabels
   356  	if includeLabels || options.WithNamespace || nameColumn != -1 {
   357  		for i := range rows {
   358  			row := rows[i]
   359  
   360  			if nameColumn != -1 {
   361  				row.Cells[nameColumn] = fmt.Sprintf("%s/%s", strings.ToLower(options.Kind.String()), row.Cells[nameColumn])
   362  			}
   363  
   364  			var m metav1.Object
   365  			if obj := row.Object.Object; obj != nil {
   366  				if acc, err := meta.Accessor(obj); err == nil {
   367  					m = acc
   368  				}
   369  			}
   370  			// if we can't get an accessor, fill out the appropriate columns with empty spaces
   371  			if m == nil {
   372  				if options.WithNamespace {
   373  					r := make([]interface{}, 1, width)
   374  					row.Cells = append(r, row.Cells...)
   375  				}
   376  				for j := 0; j < width-len(row.Cells); j++ {
   377  					row.Cells = append(row.Cells, nil)
   378  				}
   379  				rows[i] = row
   380  				continue
   381  			}
   382  
   383  			if options.WithNamespace {
   384  				r := make([]interface{}, 1, width)
   385  				r[0] = m.GetNamespace()
   386  				row.Cells = append(r, row.Cells...)
   387  			}
   388  			if includeLabels {
   389  				row.Cells = appendLabelCells(row.Cells, m.GetLabels(), options)
   390  			}
   391  			rows[i] = row
   392  		}
   393  	}
   394  
   395  	table.ColumnDefinitions = columns
   396  	table.Rows = rows
   397  	return nil
   398  }
   399  
   400  // printRowsForHandlerEntry prints the incremental table output (headers if the current type is
   401  // different from lastType) including all the rows in the object. It returns the current type
   402  // or an error, if any.
   403  func printRowsForHandlerEntry(output io.Writer, handler *printHandler, eventType string, obj runtime.Object, options PrintOptions, includeHeaders bool) error {
   404  	var results []reflect.Value
   405  
   406  	args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
   407  	results = handler.printFunc.Call(args)
   408  	if !results[1].IsNil() {
   409  		return results[1].Interface().(error)
   410  	}
   411  
   412  	if includeHeaders {
   413  		var headers []string
   414  		for _, column := range handler.columnDefinitions {
   415  			if column.Priority != 0 && !options.Wide {
   416  				continue
   417  			}
   418  			headers = append(headers, strings.ToUpper(column.Name))
   419  		}
   420  		headers = append(headers, formatLabelHeaders(options.ColumnLabels)...)
   421  		// LABELS is always the last column.
   422  		headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...)
   423  		// prepend namespace header
   424  		if options.WithNamespace {
   425  			headers = append(withNamespacePrefixColumns, headers...)
   426  		}
   427  		// prepend event type header
   428  		if len(eventType) > 0 {
   429  			headers = append(withEventTypePrefixColumns, headers...)
   430  		}
   431  		printHeader(headers, output)
   432  	}
   433  
   434  	if results[1].IsNil() {
   435  		rows := results[0].Interface().([]metav1.TableRow)
   436  		printRows(output, eventType, rows, options)
   437  		return nil
   438  	}
   439  	return results[1].Interface().(error)
   440  }
   441  
   442  var formattedEventType = map[string]string{
   443  	string(watch.Added):    "ADDED   ",
   444  	string(watch.Modified): "MODIFIED",
   445  	string(watch.Deleted):  "DELETED ",
   446  	string(watch.Error):    "ERROR   ",
   447  }
   448  
   449  func formatEventType(eventType string) string {
   450  	if formatted, ok := formattedEventType[eventType]; ok {
   451  		return formatted
   452  	}
   453  	return eventType
   454  }
   455  
   456  // printRows writes the provided rows to output.
   457  func printRows(output io.Writer, eventType string, rows []metav1.TableRow, options PrintOptions) {
   458  	for _, row := range rows {
   459  		if len(eventType) > 0 {
   460  			fmt.Fprint(output, formatEventType(eventType))
   461  			fmt.Fprint(output, "\t")
   462  		}
   463  		if options.WithNamespace {
   464  			if obj := row.Object.Object; obj != nil {
   465  				if m, err := meta.Accessor(obj); err == nil {
   466  					fmt.Fprint(output, m.GetNamespace())
   467  				}
   468  			}
   469  			fmt.Fprint(output, "\t")
   470  		}
   471  
   472  		for i, cell := range row.Cells {
   473  			if i != 0 {
   474  				fmt.Fprint(output, "\t")
   475  			} else {
   476  				// TODO: remove this once we drop the legacy printers
   477  				if options.WithKind && !options.Kind.Empty() {
   478  					fmt.Fprintf(output, "%s/%s", strings.ToLower(options.Kind.String()), cell)
   479  					continue
   480  				}
   481  			}
   482  			fmt.Fprint(output, cell)
   483  		}
   484  
   485  		hasLabels := len(options.ColumnLabels) > 0
   486  		if obj := row.Object.Object; obj != nil && (hasLabels || options.ShowLabels) {
   487  			if m, err := meta.Accessor(obj); err == nil {
   488  				for _, value := range labelValues(m.GetLabels(), options) {
   489  					output.Write([]byte("\t"))
   490  					output.Write([]byte(value))
   491  				}
   492  			}
   493  		}
   494  
   495  		output.Write([]byte("\n"))
   496  	}
   497  }
   498  
   499  func formatLabelHeaders(columnLabels []string) []string {
   500  	formHead := make([]string, len(columnLabels))
   501  	for i, l := range columnLabels {
   502  		p := strings.Split(l, "/")
   503  		formHead[i] = strings.ToUpper(p[len(p)-1])
   504  	}
   505  	return formHead
   506  }
   507  
   508  // headers for --show-labels=true
   509  func formatShowLabelsHeader(showLabels bool) []string {
   510  	if showLabels {
   511  		return []string{"LABELS"}
   512  	}
   513  	return nil
   514  }
   515  
   516  // labelValues returns a slice of value columns matching the requested print options.
   517  func labelValues(itemLabels map[string]string, opts PrintOptions) []string {
   518  	var values []string
   519  	for _, key := range opts.ColumnLabels {
   520  		values = append(values, itemLabels[key])
   521  	}
   522  	if opts.ShowLabels {
   523  		values = append(values, labels.FormatLabels(itemLabels))
   524  	}
   525  	return values
   526  }
   527  
   528  // appendLabelCells returns a slice of value columns matching the requested print options.
   529  // Intended for use with tables.
   530  func appendLabelCells(values []interface{}, itemLabels map[string]string, opts PrintOptions) []interface{} {
   531  	for _, key := range opts.ColumnLabels {
   532  		values = append(values, itemLabels[key])
   533  	}
   534  	if opts.ShowLabels {
   535  		values = append(values, labels.FormatLabels(itemLabels))
   536  	}
   537  	return values
   538  }
   539  
   540  func printStatus(obj runtime.Object, options PrintOptions) ([]metav1.TableRow, error) {
   541  	status, ok := obj.(*metav1.Status)
   542  	if !ok {
   543  		return nil, fmt.Errorf("expected *v1.Status, got %T", obj)
   544  	}
   545  	return []metav1.TableRow{{
   546  		Object: runtime.RawExtension{Object: obj},
   547  		Cells:  []interface{}{status.Status, status.Reason, status.Message},
   548  	}}, nil
   549  }
   550  
   551  func printObjectMeta(obj runtime.Object, options PrintOptions) ([]metav1.TableRow, error) {
   552  	if meta.IsListType(obj) {
   553  		rows := make([]metav1.TableRow, 0, 16)
   554  		err := meta.EachListItem(obj, func(obj runtime.Object) error {
   555  			nestedRows, err := printObjectMeta(obj, options)
   556  			if err != nil {
   557  				return err
   558  			}
   559  			rows = append(rows, nestedRows...)
   560  			return nil
   561  		})
   562  		if err != nil {
   563  			return nil, err
   564  		}
   565  		return rows, nil
   566  	}
   567  
   568  	rows := make([]metav1.TableRow, 0, 1)
   569  	m, err := meta.Accessor(obj)
   570  	if err != nil {
   571  		return nil, err
   572  	}
   573  	row := metav1.TableRow{
   574  		Object: runtime.RawExtension{Object: obj},
   575  	}
   576  	row.Cells = append(row.Cells, m.GetName(), translateTimestampSince(m.GetCreationTimestamp()))
   577  	rows = append(rows, row)
   578  	return rows, nil
   579  }
   580  
   581  // translateTimestampSince returns the elapsed time since timestamp in
   582  // human-readable approximation.
   583  func translateTimestampSince(timestamp metav1.Time) string {
   584  	if timestamp.IsZero() {
   585  		return "<unknown>"
   586  	}
   587  
   588  	return duration.HumanDuration(time.Since(timestamp.Time))
   589  }
   590  

View as plain text