...

Source file src/k8s.io/kubectl/pkg/cmd/get/customcolumn.go

Documentation: k8s.io/kubectl/pkg/cmd/get

     1  /*
     2  Copyright 2014 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 get
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"fmt"
    23  	"io"
    24  	"reflect"
    25  	"regexp"
    26  	"strings"
    27  
    28  	"github.com/liggitt/tabwriter"
    29  
    30  	"k8s.io/apimachinery/pkg/api/meta"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/cli-runtime/pkg/printers"
    35  	"k8s.io/client-go/util/jsonpath"
    36  )
    37  
    38  var jsonRegexp = regexp.MustCompile(`^\{\.?([^{}]+)\}$|^\.?([^{}]+)$`)
    39  
    40  // RelaxedJSONPathExpression attempts to be flexible with JSONPath expressions, it accepts:
    41  //   - metadata.name (no leading '.' or curly braces '{...}'
    42  //   - {metadata.name} (no leading '.')
    43  //   - .metadata.name (no curly braces '{...}')
    44  //   - {.metadata.name} (complete expression)
    45  //
    46  // And transforms them all into a valid jsonpath expression:
    47  //
    48  //	{.metadata.name}
    49  func RelaxedJSONPathExpression(pathExpression string) (string, error) {
    50  	if len(pathExpression) == 0 {
    51  		return pathExpression, nil
    52  	}
    53  	submatches := jsonRegexp.FindStringSubmatch(pathExpression)
    54  	if submatches == nil {
    55  		return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'")
    56  	}
    57  	if len(submatches) != 3 {
    58  		return "", fmt.Errorf("unexpected submatch list: %v", submatches)
    59  	}
    60  	var fieldSpec string
    61  	if len(submatches[1]) != 0 {
    62  		fieldSpec = submatches[1]
    63  	} else {
    64  		fieldSpec = submatches[2]
    65  	}
    66  	return fmt.Sprintf("{.%s}", fieldSpec), nil
    67  }
    68  
    69  // NewCustomColumnsPrinterFromSpec creates a custom columns printer from a comma separated list of <header>:<jsonpath-field-spec> pairs.
    70  // e.g. NAME:metadata.name,API_VERSION:apiVersion creates a printer that prints:
    71  //
    72  //	NAME               API_VERSION
    73  //	foo                bar
    74  func NewCustomColumnsPrinterFromSpec(spec string, decoder runtime.Decoder, noHeaders bool) (*CustomColumnsPrinter, error) {
    75  	if len(spec) == 0 {
    76  		return nil, fmt.Errorf("custom-columns format specified but no custom columns given")
    77  	}
    78  	parts := strings.Split(spec, ",")
    79  	columns := make([]Column, len(parts))
    80  	for ix := range parts {
    81  		colSpec := strings.SplitN(parts[ix], ":", 2)
    82  		if len(colSpec) != 2 {
    83  			return nil, fmt.Errorf("unexpected custom-columns spec: %s, expected <header>:<json-path-expr>", parts[ix])
    84  		}
    85  		spec, err := RelaxedJSONPathExpression(colSpec[1])
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		columns[ix] = Column{Header: colSpec[0], FieldSpec: spec}
    90  	}
    91  	return &CustomColumnsPrinter{Columns: columns, Decoder: decoder, NoHeaders: noHeaders}, nil
    92  }
    93  
    94  func splitOnWhitespace(line string) []string {
    95  	lineScanner := bufio.NewScanner(bytes.NewBufferString(line))
    96  	lineScanner.Split(bufio.ScanWords)
    97  	result := []string{}
    98  	for lineScanner.Scan() {
    99  		result = append(result, lineScanner.Text())
   100  	}
   101  	return result
   102  }
   103  
   104  // NewCustomColumnsPrinterFromTemplate creates a custom columns printer from a template stream.  The template is expected
   105  // to consist of two lines, whitespace separated.  The first line is the header line, the second line is the jsonpath field spec
   106  // For example, the template below:
   107  // NAME               API_VERSION
   108  // {metadata.name}    {apiVersion}
   109  func NewCustomColumnsPrinterFromTemplate(templateReader io.Reader, decoder runtime.Decoder) (*CustomColumnsPrinter, error) {
   110  	scanner := bufio.NewScanner(templateReader)
   111  	if !scanner.Scan() {
   112  		return nil, fmt.Errorf("invalid template, missing header line. Expected format is one line of space separated headers, one line of space separated column specs.")
   113  	}
   114  	headers := splitOnWhitespace(scanner.Text())
   115  
   116  	if !scanner.Scan() {
   117  		return nil, fmt.Errorf("invalid template, missing spec line. Expected format is one line of space separated headers, one line of space separated column specs.")
   118  	}
   119  	specs := splitOnWhitespace(scanner.Text())
   120  
   121  	if len(headers) != len(specs) {
   122  		return nil, fmt.Errorf("number of headers (%d) and field specifications (%d) don't match", len(headers), len(specs))
   123  	}
   124  
   125  	columns := make([]Column, len(headers))
   126  	for ix := range headers {
   127  		spec, err := RelaxedJSONPathExpression(specs[ix])
   128  		if err != nil {
   129  			return nil, err
   130  		}
   131  		columns[ix] = Column{
   132  			Header:    headers[ix],
   133  			FieldSpec: spec,
   134  		}
   135  	}
   136  	return &CustomColumnsPrinter{Columns: columns, Decoder: decoder, NoHeaders: false}, nil
   137  }
   138  
   139  // Column represents a user specified column
   140  type Column struct {
   141  	// The header to print above the column, general style is ALL_CAPS
   142  	Header string
   143  	// The pointer to the field in the object to print in JSONPath form
   144  	// e.g. {.ObjectMeta.Name}, see pkg/util/jsonpath for more details.
   145  	FieldSpec string
   146  }
   147  
   148  // CustomColumnPrinter is a printer that knows how to print arbitrary columns
   149  // of data from templates specified in the `Columns` array
   150  type CustomColumnsPrinter struct {
   151  	Columns   []Column
   152  	Decoder   runtime.Decoder
   153  	NoHeaders bool
   154  	// lastType records type of resource printed last so that we don't repeat
   155  	// header while printing same type of resources.
   156  	lastType reflect.Type
   157  }
   158  
   159  func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
   160  	// we use reflect.Indirect here in order to obtain the actual value from a pointer.
   161  	// we need an actual value in order to retrieve the package path for an object.
   162  	// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
   163  	if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
   164  		return fmt.Errorf(printers.InternalObjectPrinterErr)
   165  	}
   166  
   167  	if _, found := out.(*tabwriter.Writer); !found {
   168  		w := printers.GetNewTabWriter(out)
   169  		out = w
   170  		defer w.Flush()
   171  	}
   172  
   173  	t := reflect.TypeOf(obj)
   174  	if !s.NoHeaders && t != s.lastType {
   175  		headers := make([]string, len(s.Columns))
   176  		for ix := range s.Columns {
   177  			headers[ix] = s.Columns[ix].Header
   178  		}
   179  		fmt.Fprintln(out, strings.Join(headers, "\t"))
   180  		s.lastType = t
   181  	}
   182  	parsers := make([]*jsonpath.JSONPath, len(s.Columns))
   183  	for ix := range s.Columns {
   184  		parsers[ix] = jsonpath.New(fmt.Sprintf("column%d", ix)).AllowMissingKeys(true)
   185  		if err := parsers[ix].Parse(s.Columns[ix].FieldSpec); err != nil {
   186  			return err
   187  		}
   188  	}
   189  
   190  	if meta.IsListType(obj) {
   191  		objs, err := meta.ExtractList(obj)
   192  		if err != nil {
   193  			return err
   194  		}
   195  		for ix := range objs {
   196  			if err := s.printOneObject(objs[ix], parsers, out); err != nil {
   197  				return err
   198  			}
   199  		}
   200  	} else {
   201  		if err := s.printOneObject(obj, parsers, out); err != nil {
   202  			return err
   203  		}
   204  	}
   205  	return nil
   206  }
   207  
   208  func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jsonpath.JSONPath, out io.Writer) error {
   209  	columns := make([]string, len(parsers))
   210  	switch u := obj.(type) {
   211  	case *metav1.WatchEvent:
   212  		if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(u.Object.Object)).Type().PkgPath()) {
   213  			return fmt.Errorf(printers.InternalObjectPrinterErr)
   214  		}
   215  		unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(u.Object.Object)
   216  		if err != nil {
   217  			return err
   218  		}
   219  		obj = &unstructured.Unstructured{
   220  			Object: map[string]interface{}{
   221  				"type":   u.Type,
   222  				"object": unstructuredObject,
   223  			},
   224  		}
   225  
   226  	case *runtime.Unknown:
   227  		if len(u.Raw) > 0 {
   228  			var err error
   229  			if obj, err = runtime.Decode(s.Decoder, u.Raw); err != nil {
   230  				return fmt.Errorf("can't decode object for printing: %v (%s)", err, u.Raw)
   231  			}
   232  		}
   233  	}
   234  
   235  	for ix := range parsers {
   236  		parser := parsers[ix]
   237  
   238  		var values [][]reflect.Value
   239  		var err error
   240  		if unstructured, ok := obj.(runtime.Unstructured); ok {
   241  			values, err = parser.FindResults(unstructured.UnstructuredContent())
   242  		} else {
   243  			values, err = parser.FindResults(reflect.ValueOf(obj).Elem().Interface())
   244  		}
   245  
   246  		if err != nil {
   247  			return err
   248  		}
   249  		valueStrings := []string{}
   250  		if len(values) == 0 || len(values[0]) == 0 {
   251  			valueStrings = append(valueStrings, "<none>")
   252  		}
   253  		for arrIx := range values {
   254  			for valIx := range values[arrIx] {
   255  				valueStrings = append(valueStrings, printers.EscapeTerminal(fmt.Sprint(values[arrIx][valIx].Interface())))
   256  			}
   257  		}
   258  		columns[ix] = strings.Join(valueStrings, ",")
   259  	}
   260  	fmt.Fprintln(out, strings.Join(columns, "\t"))
   261  	return nil
   262  }
   263  

View as plain text