mirror of https://github.com/k3s-io/k3s
405 lines
13 KiB
Go
405 lines
13 KiB
Go
/*
|
|
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 printers
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/fatih/camelcase"
|
|
"github.com/golang/glog"
|
|
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/kubernetes/pkg/util/slice"
|
|
)
|
|
|
|
var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
|
|
|
|
type handlerEntry struct {
|
|
columns []string
|
|
columnsWithWide []string
|
|
printFunc reflect.Value
|
|
args []reflect.Value
|
|
}
|
|
|
|
// HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide
|
|
// more elegant output. It is not threadsafe, but you may call PrintObj repeatedly; headers
|
|
// will only be printed if the object type changes. This makes it useful for printing items
|
|
// received from watches.
|
|
type HumanReadablePrinter struct {
|
|
handlerMap map[reflect.Type]*handlerEntry
|
|
options PrintOptions
|
|
lastType reflect.Type
|
|
hiddenObjNum int
|
|
encoder runtime.Encoder
|
|
decoder runtime.Decoder
|
|
}
|
|
|
|
// 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 {
|
|
printer := &HumanReadablePrinter{
|
|
handlerMap: make(map[reflect.Type]*handlerEntry),
|
|
options: options,
|
|
encoder: encoder,
|
|
decoder: decoder,
|
|
}
|
|
return printer
|
|
}
|
|
|
|
// GetResourceKind returns the type currently set for a resource
|
|
func (h *HumanReadablePrinter) GetResourceKind() string {
|
|
return h.options.Kind
|
|
}
|
|
|
|
// EnsurePrintWithKind sets HumanReadablePrinter options "WithKind" to true
|
|
// and "Kind" to the string arg it receives, pre-pending this string
|
|
// to the "NAME" column in an output of resources.
|
|
func (h *HumanReadablePrinter) EnsurePrintWithKind(kind string) {
|
|
h.options.WithKind = true
|
|
h.options.Kind = kind
|
|
}
|
|
|
|
// EnsurePrintHeaders sets the HumanReadablePrinter option "NoHeaders" to false
|
|
// and removes the .lastType that was printed, which forces headers to be
|
|
// printed in cases where multiple lists of the same resource are printed
|
|
// consecutively, but are separated by non-printer related information.
|
|
func (h *HumanReadablePrinter) EnsurePrintHeaders() {
|
|
h.options.NoHeaders = false
|
|
h.lastType = nil
|
|
}
|
|
|
|
// Handler adds a print handler with a given set of columns to HumanReadablePrinter instance.
|
|
// See validatePrintHandlerFunc for required method signature.
|
|
func (h *HumanReadablePrinter) Handler(columns, columnsWithWide []string, printFunc interface{}) error {
|
|
printFuncValue := reflect.ValueOf(printFunc)
|
|
if err := h.validatePrintHandlerFunc(printFuncValue); err != nil {
|
|
glog.Errorf("Unable to add print handler: %v", err)
|
|
return err
|
|
}
|
|
|
|
objType := printFuncValue.Type().In(0)
|
|
h.handlerMap[objType] = &handlerEntry{
|
|
columns: columns,
|
|
columnsWithWide: columnsWithWide,
|
|
printFunc: printFuncValue,
|
|
}
|
|
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 {
|
|
if printFunc.Kind() != reflect.Func {
|
|
return fmt.Errorf("invalid print handler. %#v is not a function", printFunc)
|
|
}
|
|
funcType := printFunc.Type()
|
|
if funcType.NumIn() != 3 || funcType.NumOut() != 1 {
|
|
return fmt.Errorf("invalid print handler." +
|
|
"Must accept 3 parameters and return 1 value.")
|
|
}
|
|
if funcType.In(1) != reflect.TypeOf((*io.Writer)(nil)).Elem() ||
|
|
funcType.In(2) != reflect.TypeOf((*PrintOptions)(nil)).Elem() ||
|
|
funcType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() {
|
|
return fmt.Errorf("invalid print handler. The expected signature is: "+
|
|
"func handler(obj %v, w io.Writer, options PrintOptions) error", funcType.In(0))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *HumanReadablePrinter) HandledResources() []string {
|
|
keys := make([]string, 0)
|
|
|
|
for k := range h.handlerMap {
|
|
// k.String looks like "*api.PodList" and we want just "pod"
|
|
api := strings.Split(k.String(), ".")
|
|
resource := api[len(api)-1]
|
|
if strings.HasSuffix(resource, "List") {
|
|
continue
|
|
}
|
|
resource = strings.ToLower(resource)
|
|
keys = append(keys, resource)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (h *HumanReadablePrinter) AfterPrint(output io.Writer, res string) error {
|
|
return nil
|
|
}
|
|
|
|
func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error {
|
|
_, err := fmt.Fprintf(w, "Unknown object: %s", string(data))
|
|
return err
|
|
}
|
|
|
|
func (h *HumanReadablePrinter) 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 {
|
|
// 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)
|
|
defer w.Flush()
|
|
}
|
|
|
|
// 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 {
|
|
obj, _ = decodeUnknownObject(obj, h.encoder, h.decoder)
|
|
}
|
|
|
|
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...)
|
|
}
|
|
headers = append(headers, formatLabelHeaders(h.options.ColumnLabels)...)
|
|
// LABELS is always the last column.
|
|
headers = append(headers, formatShowLabelsHeader(h.options.ShowLabels, t)...)
|
|
if h.options.WithNamespace {
|
|
headers = append(withNamespacePrefixColumns, headers...)
|
|
}
|
|
h.printHeader(headers, w)
|
|
h.lastType = t
|
|
}
|
|
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(w), reflect.ValueOf(h.options)}
|
|
resultValue := handler.printFunc.Call(args)[0]
|
|
if resultValue.IsNil() {
|
|
return nil
|
|
}
|
|
return resultValue.Interface().(error)
|
|
}
|
|
|
|
if _, err := meta.Accessor(obj); err == nil {
|
|
// 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)
|
|
}
|
|
|
|
content := unstructured.UnstructuredContent()
|
|
|
|
// we'll elect a few more fields to print depending on how much columns are already taken
|
|
maxDiscoveredFieldsToPrint := 3
|
|
maxDiscoveredFieldsToPrint = maxDiscoveredFieldsToPrint - len(h.options.ColumnLabels)
|
|
if h.options.WithNamespace { // where's my ternary
|
|
maxDiscoveredFieldsToPrint--
|
|
}
|
|
if h.options.ShowLabels {
|
|
maxDiscoveredFieldsToPrint--
|
|
}
|
|
if maxDiscoveredFieldsToPrint < 0 {
|
|
maxDiscoveredFieldsToPrint = 0
|
|
}
|
|
|
|
var discoveredFieldNames []string // we want it predictable so this will be used to sort
|
|
ignoreIfDiscovered := []string{"kind", "apiVersion"} // these are already covered
|
|
for field, value := range content {
|
|
if slice.ContainsString(ignoreIfDiscovered, field, nil) {
|
|
continue
|
|
}
|
|
switch value.(type) {
|
|
case map[string]interface{}:
|
|
// just simpler types
|
|
continue
|
|
}
|
|
discoveredFieldNames = append(discoveredFieldNames, field)
|
|
}
|
|
sort.Strings(discoveredFieldNames)
|
|
if len(discoveredFieldNames) > maxDiscoveredFieldsToPrint {
|
|
discoveredFieldNames = discoveredFieldNames[:maxDiscoveredFieldsToPrint]
|
|
}
|
|
|
|
if !h.options.NoHeaders && t != h.lastType {
|
|
headers := []string{"NAME", "KIND"}
|
|
for _, discoveredField := range discoveredFieldNames {
|
|
fieldAsHeader := strings.ToUpper(strings.Join(camelcase.Split(discoveredField), " "))
|
|
headers = append(headers, fieldAsHeader)
|
|
}
|
|
headers = append(headers, formatLabelHeaders(h.options.ColumnLabels)...)
|
|
// LABELS is always the last column.
|
|
headers = append(headers, formatShowLabelsHeader(h.options.ShowLabels, t)...)
|
|
if h.options.WithNamespace {
|
|
headers = append(withNamespacePrefixColumns, headers...)
|
|
}
|
|
h.printHeader(headers, w)
|
|
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 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// we failed all reasonable printing efforts, report failure
|
|
return fmt.Errorf("error: unknown type %#v", obj)
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if options.WithNamespace {
|
|
if _, err := fmt.Fprintf(w, "%s\t", metadata.GetNamespace()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
content := unstructured.UnstructuredContent()
|
|
kind := "<missing>"
|
|
if objKind, ok := content["kind"]; ok {
|
|
if str, ok := objKind.(string); ok {
|
|
kind = str
|
|
}
|
|
}
|
|
if objAPIVersion, ok := content["apiVersion"]; ok {
|
|
if str, ok := objAPIVersion.(string); ok {
|
|
version, err := schema.ParseGroupVersion(str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kind = kind + "." + version.Version + "." + version.Group
|
|
}
|
|
}
|
|
|
|
name := FormatResourceName(options.Kind, metadata.GetName(), options.WithKind)
|
|
|
|
if _, err := fmt.Fprintf(w, "%s\t%s", name, kind); err != nil {
|
|
return err
|
|
}
|
|
for _, field := range additionalFields {
|
|
if value, ok := content[field]; ok {
|
|
var formattedValue string
|
|
switch typedValue := value.(type) {
|
|
case []interface{}:
|
|
formattedValue = fmt.Sprintf("%d item(s)", len(typedValue))
|
|
default:
|
|
formattedValue = fmt.Sprintf("%v", value)
|
|
}
|
|
if _, err := fmt.Fprintf(w, "\t%s", formattedValue); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if _, err := fmt.Fprint(w, AppendLabels(metadata.GetLabels(), options.ColumnLabels)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprint(w, AppendAllLabels(options.ShowLabels, metadata.GetLabels())); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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, t reflect.Type) []string {
|
|
if showLabels {
|
|
// TODO: this is all sorts of hack, fix
|
|
if t.String() != "*api.ThirdPartyResource" && t.String() != "*api.ThirdPartyResourceList" {
|
|
return []string{"LABELS"}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
if !withKind || kind == "" {
|
|
return name
|
|
}
|
|
|
|
return kind + "/" + name
|
|
}
|
|
|
|
func AppendLabels(itemLabels map[string]string, columnLabels []string) string {
|
|
var buffer bytes.Buffer
|
|
|
|
for _, cl := range columnLabels {
|
|
buffer.WriteString(fmt.Sprint("\t"))
|
|
if il, ok := itemLabels[cl]; ok {
|
|
buffer.WriteString(fmt.Sprint(il))
|
|
} else {
|
|
buffer.WriteString("<none>")
|
|
}
|
|
}
|
|
|
|
return buffer.String()
|
|
}
|
|
|
|
// Append all labels to a single column. We need this even when show-labels flag* is
|
|
// false, since this adds newline delimiter to the end of each row.
|
|
func AppendAllLabels(showLabels bool, itemLabels map[string]string) string {
|
|
var buffer bytes.Buffer
|
|
|
|
if showLabels {
|
|
buffer.WriteString(fmt.Sprint("\t"))
|
|
buffer.WriteString(labels.FormatLabels(itemLabels))
|
|
}
|
|
buffer.WriteString("\n")
|
|
|
|
return buffer.String()
|
|
}
|
|
|
|
// check if the object is unstructured. If so, attempt to convert it to a type we can understand.
|
|
func decodeUnknownObject(obj runtime.Object, encoder runtime.Encoder, decoder runtime.Decoder) (runtime.Object, error) {
|
|
var err error
|
|
switch obj.(type) {
|
|
case runtime.Unstructured, *runtime.Unknown:
|
|
if objBytes, err := runtime.Encode(encoder, obj); err == nil {
|
|
if decodedObj, err := runtime.Decode(decoder, objBytes); err == nil {
|
|
obj = decodedObj
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj, err
|
|
}
|