cli-utils/pkg/print/table/columndefs.go

245 lines
6.6 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package table
import (
"fmt"
"io"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/integer"
"github.com/fluxcd/cli-utils/pkg/print/common"
)
// ColumnDef is an implementation of the ColumnDefinition interface.
// It can be used to define simple columns that doesn't need additional
// knowledge about the actual type of the provided Resource besides the
// information available through the interface.
type ColumnDef struct {
ColumnName string
ColumnHeader string
ColumnWidth int
PrintResourceFunc func(w io.Writer, width int, r Resource) (int, error)
}
// Name returns the name of the column.
func (c ColumnDef) Name() string {
return c.ColumnName
}
// Header returns the header that should be printed for
// the column.
func (c ColumnDef) Header() string {
return c.ColumnHeader
}
// Width returns the width of the column.
func (c ColumnDef) Width() int {
return c.ColumnWidth
}
// PrintResource is called by the BaseTablePrinter to output the
// content of a particular column. This implementation just delegates
// to the provided PrintResourceFunc.
func (c ColumnDef) PrintResource(w io.Writer, width int, r Resource) (int, error) {
return c.PrintResourceFunc(w, width, r)
}
// MustColumn returns the pre-defined column definition with the
// provided name. If the name doesn't exist, it will panic.
func MustColumn(name string) ColumnDef {
c, found := columnDefinitions[name]
if !found {
panic(fmt.Errorf("unknown column name %q", name))
}
return c
}
var (
columnDefinitions = map[string]ColumnDef{
// namespace defines a column that output the namespace of the
// resource, or nothing in the case of clusterscoped resources.
"namespace": {
ColumnName: "namespace",
ColumnHeader: "NAMESPACE",
ColumnWidth: 10,
PrintResourceFunc: func(w io.Writer, width int, r Resource) (int,
error) {
namespace := r.Identifier().Namespace
if len(namespace) > width {
namespace = namespace[:width]
}
_, err := fmt.Fprint(w, namespace)
return len(namespace), err
},
},
// resource defines a column that outputs the kind and name of a
// resource.
"resource": {
ColumnName: "resource",
ColumnHeader: "RESOURCE",
ColumnWidth: 40,
PrintResourceFunc: func(w io.Writer, width int, r Resource) (int,
error) {
text := fmt.Sprintf("%s/%s", r.Identifier().GroupKind.Kind,
r.Identifier().Name)
if len(text) > width {
text = text[:width]
}
_, err := fmt.Fprint(w, text)
return len(text), err
},
},
// status defines a column that outputs the status of a resource. It
// will use ansii escape codes to color the output.
"status": {
ColumnName: "status",
ColumnHeader: "STATUS",
ColumnWidth: 10,
PrintResourceFunc: func(w io.Writer, width int, r Resource) (int,
error) {
rs := r.ResourceStatus()
if rs == nil {
return 0, nil
}
s := rs.Status.String()
if len(s) > width {
s = s[:width]
}
color, setColor := common.ColorForStatus(rs.Status)
var outputStatus string
if setColor {
outputStatus = common.SprintfWithColor(color, "%s", s)
} else {
outputStatus = s
}
_, err := fmt.Fprint(w, outputStatus)
return len(s), err
},
},
// conditions defines a column that outputs the conditions for
// a resource. The output will be in colors.
"conditions": {
ColumnName: "conditions",
ColumnHeader: "CONDITIONS",
ColumnWidth: 40,
PrintResourceFunc: func(w io.Writer, width int, r Resource) (int,
error) {
rs := r.ResourceStatus()
if rs == nil {
return 0, nil
}
u := rs.Resource
if u == nil {
return fmt.Fprintf(w, "-")
}
conditions, found, err := unstructured.NestedSlice(u.Object,
"status", "conditions")
if !found || err != nil || len(conditions) == 0 {
return fmt.Fprintf(w, "<None>")
}
realLength := 0
for i, cond := range conditions {
condition := cond.(map[string]interface{})
conditionType := condition["type"].(string)
conditionStatus := condition["status"].(string)
var color common.Color
switch conditionStatus {
case "True":
color = common.GREEN
case "False":
color = common.RED
default:
color = common.YELLOW
}
remainingWidth := width - realLength
if len(conditionType) > remainingWidth {
conditionType = conditionType[:remainingWidth]
}
_, err := fmt.Fprint(w, common.SprintfWithColor(color, "%s", conditionType))
if err != nil {
return realLength, err
}
realLength += len(conditionType)
if i < len(conditions)-1 && width-realLength > 2 {
_, err = fmt.Fprintf(w, ",")
if err != nil {
return realLength, err
}
realLength++
}
}
return realLength, nil
},
},
// age defines a column that outputs the age of a resource computed
// by looking at the creationTimestamp field.
"age": {
ColumnName: "age",
ColumnHeader: "AGE",
ColumnWidth: 6,
PrintResourceFunc: func(w io.Writer, width int, r Resource) (i int, err error) {
rs := r.ResourceStatus()
if rs == nil {
return 0, nil
}
u := rs.Resource
if u == nil {
return fmt.Fprint(w, "-")
}
timestamp, found, err := unstructured.NestedString(u.Object,
"metadata", "creationTimestamp")
if !found || err != nil || timestamp == "" {
return fmt.Fprint(w, "-")
}
parsedTime, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
return fmt.Fprint(w, "-")
}
age := time.Since(parsedTime)
switch {
case age.Seconds() <= 90:
return fmt.Fprintf(w, "%ds",
integer.RoundToInt32(age.Round(time.Second).Seconds()))
case age.Minutes() <= 90:
return fmt.Fprintf(w, "%dm",
integer.RoundToInt32(age.Round(time.Minute).Minutes()))
default:
return fmt.Fprintf(w, "%dh",
integer.RoundToInt32(age.Round(time.Hour).Hours()))
}
},
},
// message defines a column that outputs the message from a
// ResourceStatus, or if there is a non-nil error, output the text
// from the error instead.
"message": {
ColumnName: "message",
ColumnHeader: "MESSAGE",
ColumnWidth: 40,
PrintResourceFunc: func(w io.Writer, width int, r Resource) (i int, err error) {
rs := r.ResourceStatus()
if rs == nil {
return 0, nil
}
var message string
if rs.Error != nil {
message = rs.Error.Error()
} else {
message = rs.Message
}
if len(message) > width {
message = message[:width]
}
return fmt.Fprint(w, message)
},
},
}
)