1/* 2Copyright 2019 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package printers 18 19import ( 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 35var _ ResourcePrinter = &HumanReadablePrinter{} 36 37type printHandler struct { 38 columnDefinitions []metav1.TableColumnDefinition 39 printFunc reflect.Value 40} 41 42var ( 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. 72type 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(). 80func NewTablePrinter(options PrintOptions) ResourcePrinter { 81 printer := &HumanReadablePrinter{ 82 options: options, 83 } 84 return printer 85} 86 87func 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. 95func (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. 172func 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 newlines 216 newline := strings.Index(print, "\n") 217 if newline >= 0 { 218 truncated = true 219 print = print[:newline] 220 } 221 fmt.Fprint(output, print) 222 if truncated { 223 fmt.Fprint(output, "...") 224 } 225 default: 226 fmt.Fprint(output, val) 227 } 228 } 229 } 230 fmt.Fprintln(output) 231 } 232 return nil 233} 234 235type cellValueFunc func(metav1.TableRow) (interface{}, error) 236 237type columnAddPosition int 238 239const ( 240 beginning columnAddPosition = 1 241 end columnAddPosition = 2 242) 243 244func addColumns(pos columnAddPosition, table *metav1.Table, columns []metav1.TableColumnDefinition, valueFuncs []cellValueFunc) error { 245 if len(columns) != len(valueFuncs) { 246 return fmt.Errorf("cannot prepend columns, unmatched value functions") 247 } 248 if len(columns) == 0 { 249 return nil 250 } 251 252 // Compute the new rows 253 newRows := make([][]interface{}, len(table.Rows)) 254 for i := range table.Rows { 255 newCells := make([]interface{}, 0, len(columns)+len(table.Rows[i].Cells)) 256 257 if pos == end { 258 // If we're appending, start with the existing cells, 259 // then add nil cells to match the number of columns 260 newCells = append(newCells, table.Rows[i].Cells...) 261 for len(newCells) < len(table.ColumnDefinitions) { 262 newCells = append(newCells, nil) 263 } 264 } 265 266 // Compute cells for new columns 267 for _, f := range valueFuncs { 268 newCell, err := f(table.Rows[i]) 269 if err != nil { 270 return err 271 } 272 newCells = append(newCells, newCell) 273 } 274 275 if pos == beginning { 276 // If we're prepending, add existing cells 277 newCells = append(newCells, table.Rows[i].Cells...) 278 } 279 280 // Remember the new cells for this row 281 newRows[i] = newCells 282 } 283 284 // All cells successfully computed, now replace columns and rows 285 newColumns := make([]metav1.TableColumnDefinition, 0, len(columns)+len(table.ColumnDefinitions)) 286 switch pos { 287 case beginning: 288 newColumns = append(newColumns, columns...) 289 newColumns = append(newColumns, table.ColumnDefinitions...) 290 case end: 291 newColumns = append(newColumns, table.ColumnDefinitions...) 292 newColumns = append(newColumns, columns...) 293 default: 294 return fmt.Errorf("invalid column add position: %v", pos) 295 } 296 table.ColumnDefinitions = newColumns 297 for i := range table.Rows { 298 table.Rows[i].Cells = newRows[i] 299 } 300 301 return nil 302} 303 304// decorateTable takes a table and attempts to add label columns and the 305// namespace column. It will fill empty columns with nil (if the object 306// does not expose metadata). It returns an error if the table cannot 307// be decorated. 308func decorateTable(table *metav1.Table, options PrintOptions) error { 309 width := len(table.ColumnDefinitions) + len(options.ColumnLabels) 310 if options.WithNamespace { 311 width++ 312 } 313 if options.ShowLabels { 314 width++ 315 } 316 317 columns := table.ColumnDefinitions 318 319 nameColumn := -1 320 if options.WithKind && !options.Kind.Empty() { 321 for i := range columns { 322 if columns[i].Format == "name" && columns[i].Type == "string" { 323 nameColumn = i 324 break 325 } 326 } 327 } 328 329 if width != len(table.ColumnDefinitions) { 330 columns = make([]metav1.TableColumnDefinition, 0, width) 331 if options.WithNamespace { 332 columns = append(columns, metav1.TableColumnDefinition{ 333 Name: "Namespace", 334 Type: "string", 335 }) 336 } 337 columns = append(columns, table.ColumnDefinitions...) 338 for _, label := range formatLabelHeaders(options.ColumnLabels) { 339 columns = append(columns, metav1.TableColumnDefinition{ 340 Name: label, 341 Type: "string", 342 }) 343 } 344 if options.ShowLabels { 345 columns = append(columns, metav1.TableColumnDefinition{ 346 Name: "Labels", 347 Type: "string", 348 }) 349 } 350 } 351 352 rows := table.Rows 353 354 includeLabels := len(options.ColumnLabels) > 0 || options.ShowLabels 355 if includeLabels || options.WithNamespace || nameColumn != -1 { 356 for i := range rows { 357 row := rows[i] 358 359 if nameColumn != -1 { 360 row.Cells[nameColumn] = fmt.Sprintf("%s/%s", strings.ToLower(options.Kind.String()), row.Cells[nameColumn]) 361 } 362 363 var m metav1.Object 364 if obj := row.Object.Object; obj != nil { 365 if acc, err := meta.Accessor(obj); err == nil { 366 m = acc 367 } 368 } 369 // if we can't get an accessor, fill out the appropriate columns with empty spaces 370 if m == nil { 371 if options.WithNamespace { 372 r := make([]interface{}, 1, width) 373 row.Cells = append(r, row.Cells...) 374 } 375 for j := 0; j < width-len(row.Cells); j++ { 376 row.Cells = append(row.Cells, nil) 377 } 378 rows[i] = row 379 continue 380 } 381 382 if options.WithNamespace { 383 r := make([]interface{}, 1, width) 384 r[0] = m.GetNamespace() 385 row.Cells = append(r, row.Cells...) 386 } 387 if includeLabels { 388 row.Cells = appendLabelCells(row.Cells, m.GetLabels(), options) 389 } 390 rows[i] = row 391 } 392 } 393 394 table.ColumnDefinitions = columns 395 table.Rows = rows 396 return nil 397} 398 399// printRowsForHandlerEntry prints the incremental table output (headers if the current type is 400// different from lastType) including all the rows in the object. It returns the current type 401// or an error, if any. 402func printRowsForHandlerEntry(output io.Writer, handler *printHandler, eventType string, obj runtime.Object, options PrintOptions, includeHeaders bool) error { 403 var results []reflect.Value 404 405 args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)} 406 results = handler.printFunc.Call(args) 407 if !results[1].IsNil() { 408 return results[1].Interface().(error) 409 } 410 411 if includeHeaders { 412 var headers []string 413 for _, column := range handler.columnDefinitions { 414 if column.Priority != 0 && !options.Wide { 415 continue 416 } 417 headers = append(headers, strings.ToUpper(column.Name)) 418 } 419 headers = append(headers, formatLabelHeaders(options.ColumnLabels)...) 420 // LABELS is always the last column. 421 headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...) 422 // prepend namespace header 423 if options.WithNamespace { 424 headers = append(withNamespacePrefixColumns, headers...) 425 } 426 // prepend event type header 427 if len(eventType) > 0 { 428 headers = append(withEventTypePrefixColumns, headers...) 429 } 430 printHeader(headers, output) 431 } 432 433 if results[1].IsNil() { 434 rows := results[0].Interface().([]metav1.TableRow) 435 printRows(output, eventType, rows, options) 436 return nil 437 } 438 return results[1].Interface().(error) 439} 440 441var formattedEventType = map[string]string{ 442 string(watch.Added): "ADDED ", 443 string(watch.Modified): "MODIFIED", 444 string(watch.Deleted): "DELETED ", 445 string(watch.Error): "ERROR ", 446} 447 448func formatEventType(eventType string) string { 449 if formatted, ok := formattedEventType[eventType]; ok { 450 return formatted 451 } 452 return string(eventType) 453} 454 455// printRows writes the provided rows to output. 456func printRows(output io.Writer, eventType string, rows []metav1.TableRow, options PrintOptions) { 457 for _, row := range rows { 458 if len(eventType) > 0 { 459 fmt.Fprint(output, formatEventType(eventType)) 460 fmt.Fprint(output, "\t") 461 } 462 if options.WithNamespace { 463 if obj := row.Object.Object; obj != nil { 464 if m, err := meta.Accessor(obj); err == nil { 465 fmt.Fprint(output, m.GetNamespace()) 466 } 467 } 468 fmt.Fprint(output, "\t") 469 } 470 471 for i, cell := range row.Cells { 472 if i != 0 { 473 fmt.Fprint(output, "\t") 474 } else { 475 // TODO: remove this once we drop the legacy printers 476 if options.WithKind && !options.Kind.Empty() { 477 fmt.Fprintf(output, "%s/%s", strings.ToLower(options.Kind.String()), cell) 478 continue 479 } 480 } 481 fmt.Fprint(output, cell) 482 } 483 484 hasLabels := len(options.ColumnLabels) > 0 485 if obj := row.Object.Object; obj != nil && (hasLabels || options.ShowLabels) { 486 if m, err := meta.Accessor(obj); err == nil { 487 for _, value := range labelValues(m.GetLabels(), options) { 488 output.Write([]byte("\t")) 489 output.Write([]byte(value)) 490 } 491 } 492 } 493 494 output.Write([]byte("\n")) 495 } 496} 497 498func formatLabelHeaders(columnLabels []string) []string { 499 formHead := make([]string, len(columnLabels)) 500 for i, l := range columnLabels { 501 p := strings.Split(l, "/") 502 formHead[i] = strings.ToUpper((p[len(p)-1])) 503 } 504 return formHead 505} 506 507// headers for --show-labels=true 508func formatShowLabelsHeader(showLabels bool) []string { 509 if showLabels { 510 return []string{"LABELS"} 511 } 512 return nil 513} 514 515// labelValues returns a slice of value columns matching the requested print options. 516func labelValues(itemLabels map[string]string, opts PrintOptions) []string { 517 var values []string 518 for _, key := range opts.ColumnLabels { 519 values = append(values, itemLabels[key]) 520 } 521 if opts.ShowLabels { 522 values = append(values, labels.FormatLabels(itemLabels)) 523 } 524 return values 525} 526 527// appendLabelCells returns a slice of value columns matching the requested print options. 528// Intended for use with tables. 529func appendLabelCells(values []interface{}, itemLabels map[string]string, opts PrintOptions) []interface{} { 530 for _, key := range opts.ColumnLabels { 531 values = append(values, itemLabels[key]) 532 } 533 if opts.ShowLabels { 534 values = append(values, labels.FormatLabels(itemLabels)) 535 } 536 return values 537} 538 539func printStatus(obj runtime.Object, options PrintOptions) ([]metav1.TableRow, error) { 540 status, ok := obj.(*metav1.Status) 541 if !ok { 542 return nil, fmt.Errorf("expected *v1.Status, got %T", obj) 543 } 544 return []metav1.TableRow{{ 545 Object: runtime.RawExtension{Object: obj}, 546 Cells: []interface{}{status.Status, status.Reason, status.Message}, 547 }}, nil 548} 549 550func printObjectMeta(obj runtime.Object, options PrintOptions) ([]metav1.TableRow, error) { 551 if meta.IsListType(obj) { 552 rows := make([]metav1.TableRow, 0, 16) 553 err := meta.EachListItem(obj, func(obj runtime.Object) error { 554 nestedRows, err := printObjectMeta(obj, options) 555 if err != nil { 556 return err 557 } 558 rows = append(rows, nestedRows...) 559 return nil 560 }) 561 if err != nil { 562 return nil, err 563 } 564 return rows, nil 565 } 566 567 rows := make([]metav1.TableRow, 0, 1) 568 m, err := meta.Accessor(obj) 569 if err != nil { 570 return nil, err 571 } 572 row := metav1.TableRow{ 573 Object: runtime.RawExtension{Object: obj}, 574 } 575 row.Cells = append(row.Cells, m.GetName(), translateTimestampSince(m.GetCreationTimestamp())) 576 rows = append(rows, row) 577 return rows, nil 578} 579 580// translateTimestampSince returns the elapsed time since timestamp in 581// human-readable approximation. 582func translateTimestampSince(timestamp metav1.Time) string { 583 if timestamp.IsZero() { 584 return "<unknown>" 585 } 586 587 return duration.HumanDuration(time.Since(timestamp.Time)) 588} 589