mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 13:50:01 +00:00 
			
		
		
		
	These packages deserve to live at the top level of `k8s.io/cli-runtime`, as they are potentially of wider use than genericclioptions.
		
			
				
	
	
		
			245 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			245 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2014 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 get
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"reflect"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/liggitt/tabwriter"
 | |
| 
 | |
| 	"k8s.io/apimachinery/pkg/api/meta"
 | |
| 	"k8s.io/apimachinery/pkg/runtime"
 | |
| 	"k8s.io/cli-runtime/pkg/printers"
 | |
| 	"k8s.io/client-go/util/jsonpath"
 | |
| 	utilprinters "k8s.io/kubernetes/pkg/kubectl/util/printers"
 | |
| )
 | |
| 
 | |
| var jsonRegexp = regexp.MustCompile("^\\{\\.?([^{}]+)\\}$|^\\.?([^{}]+)$")
 | |
| 
 | |
| // RelaxedJSONPathExpression attempts to be flexible with JSONPath expressions, it accepts:
 | |
| //   * metadata.name (no leading '.' or curly braces '{...}'
 | |
| //   * {metadata.name} (no leading '.')
 | |
| //   * .metadata.name (no curly braces '{...}')
 | |
| //   * {.metadata.name} (complete expression)
 | |
| // And transforms them all into a valid jsonpath expression:
 | |
| //   {.metadata.name}
 | |
| func RelaxedJSONPathExpression(pathExpression string) (string, error) {
 | |
| 	if len(pathExpression) == 0 {
 | |
| 		return pathExpression, nil
 | |
| 	}
 | |
| 	submatches := jsonRegexp.FindStringSubmatch(pathExpression)
 | |
| 	if submatches == nil {
 | |
| 		return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'")
 | |
| 	}
 | |
| 	if len(submatches) != 3 {
 | |
| 		return "", fmt.Errorf("unexpected submatch list: %v", submatches)
 | |
| 	}
 | |
| 	var fieldSpec string
 | |
| 	if len(submatches[1]) != 0 {
 | |
| 		fieldSpec = submatches[1]
 | |
| 	} else {
 | |
| 		fieldSpec = submatches[2]
 | |
| 	}
 | |
| 	return fmt.Sprintf("{.%s}", fieldSpec), nil
 | |
| }
 | |
| 
 | |
| // NewCustomColumnsPrinterFromSpec creates a custom columns printer from a comma separated list of <header>:<jsonpath-field-spec> pairs.
 | |
| // e.g. NAME:metadata.name,API_VERSION:apiVersion creates a printer that prints:
 | |
| //
 | |
| //      NAME               API_VERSION
 | |
| //      foo                bar
 | |
| func NewCustomColumnsPrinterFromSpec(spec string, decoder runtime.Decoder, noHeaders bool) (*CustomColumnsPrinter, error) {
 | |
| 	if len(spec) == 0 {
 | |
| 		return nil, fmt.Errorf("custom-columns format specified but no custom columns given")
 | |
| 	}
 | |
| 	parts := strings.Split(spec, ",")
 | |
| 	columns := make([]Column, len(parts))
 | |
| 	for ix := range parts {
 | |
| 		colSpec := strings.SplitN(parts[ix], ":", 2)
 | |
| 		if len(colSpec) != 2 {
 | |
| 			return nil, fmt.Errorf("unexpected custom-columns spec: %s, expected <header>:<json-path-expr>", parts[ix])
 | |
| 		}
 | |
| 		spec, err := RelaxedJSONPathExpression(colSpec[1])
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		columns[ix] = Column{Header: colSpec[0], FieldSpec: spec}
 | |
| 	}
 | |
| 	return &CustomColumnsPrinter{Columns: columns, Decoder: decoder, NoHeaders: noHeaders}, nil
 | |
| }
 | |
| 
 | |
| func splitOnWhitespace(line string) []string {
 | |
| 	lineScanner := bufio.NewScanner(bytes.NewBufferString(line))
 | |
| 	lineScanner.Split(bufio.ScanWords)
 | |
| 	result := []string{}
 | |
| 	for lineScanner.Scan() {
 | |
| 		result = append(result, lineScanner.Text())
 | |
| 	}
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // NewCustomColumnsPrinterFromTemplate creates a custom columns printer from a template stream.  The template is expected
 | |
| // to consist of two lines, whitespace separated.  The first line is the header line, the second line is the jsonpath field spec
 | |
| // For example, the template below:
 | |
| // NAME               API_VERSION
 | |
| // {metadata.name}    {apiVersion}
 | |
| func NewCustomColumnsPrinterFromTemplate(templateReader io.Reader, decoder runtime.Decoder) (*CustomColumnsPrinter, error) {
 | |
| 	scanner := bufio.NewScanner(templateReader)
 | |
| 	if !scanner.Scan() {
 | |
| 		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.")
 | |
| 	}
 | |
| 	headers := splitOnWhitespace(scanner.Text())
 | |
| 
 | |
| 	if !scanner.Scan() {
 | |
| 		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.")
 | |
| 	}
 | |
| 	specs := splitOnWhitespace(scanner.Text())
 | |
| 
 | |
| 	if len(headers) != len(specs) {
 | |
| 		return nil, fmt.Errorf("number of headers (%d) and field specifications (%d) don't match", len(headers), len(specs))
 | |
| 	}
 | |
| 
 | |
| 	columns := make([]Column, len(headers))
 | |
| 	for ix := range headers {
 | |
| 		spec, err := RelaxedJSONPathExpression(specs[ix])
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		columns[ix] = Column{
 | |
| 			Header:    headers[ix],
 | |
| 			FieldSpec: spec,
 | |
| 		}
 | |
| 	}
 | |
| 	return &CustomColumnsPrinter{Columns: columns, Decoder: decoder, NoHeaders: false}, nil
 | |
| }
 | |
| 
 | |
| // Column represents a user specified column
 | |
| type Column struct {
 | |
| 	// The header to print above the column, general style is ALL_CAPS
 | |
| 	Header string
 | |
| 	// The pointer to the field in the object to print in JSONPath form
 | |
| 	// e.g. {.ObjectMeta.Name}, see pkg/util/jsonpath for more details.
 | |
| 	FieldSpec string
 | |
| }
 | |
| 
 | |
| // CustomColumnPrinter is a printer that knows how to print arbitrary columns
 | |
| // of data from templates specified in the `Columns` array
 | |
| type CustomColumnsPrinter struct {
 | |
| 	Columns   []Column
 | |
| 	Decoder   runtime.Decoder
 | |
| 	NoHeaders bool
 | |
| 	// lastType records type of resource printed last so that we don't repeat
 | |
| 	// header while printing same type of resources.
 | |
| 	lastType reflect.Type
 | |
| }
 | |
| 
 | |
| func (s *CustomColumnsPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
 | |
| 	// we use reflect.Indirect here in order to obtain the actual value from a pointer.
 | |
| 	// we need an actual value in order to retrieve the package path for an object.
 | |
| 	// using reflect.Indirect indiscriminately is valid here, as all runtime.Objects are supposed to be pointers.
 | |
| 	if printers.InternalObjectPreventer.IsForbidden(reflect.Indirect(reflect.ValueOf(obj)).Type().PkgPath()) {
 | |
| 		return fmt.Errorf(printers.InternalObjectPrinterErr)
 | |
| 	}
 | |
| 
 | |
| 	if w, found := out.(*tabwriter.Writer); !found {
 | |
| 		w = utilprinters.GetNewTabWriter(out)
 | |
| 		out = w
 | |
| 		defer w.Flush()
 | |
| 	}
 | |
| 
 | |
| 	t := reflect.TypeOf(obj)
 | |
| 	if !s.NoHeaders && t != s.lastType {
 | |
| 		headers := make([]string, len(s.Columns))
 | |
| 		for ix := range s.Columns {
 | |
| 			headers[ix] = s.Columns[ix].Header
 | |
| 		}
 | |
| 		fmt.Fprintln(out, strings.Join(headers, "\t"))
 | |
| 		s.lastType = t
 | |
| 	}
 | |
| 	parsers := make([]*jsonpath.JSONPath, len(s.Columns))
 | |
| 	for ix := range s.Columns {
 | |
| 		parsers[ix] = jsonpath.New(fmt.Sprintf("column%d", ix)).AllowMissingKeys(true)
 | |
| 		if err := parsers[ix].Parse(s.Columns[ix].FieldSpec); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if meta.IsListType(obj) {
 | |
| 		objs, err := meta.ExtractList(obj)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		for ix := range objs {
 | |
| 			if err := s.printOneObject(objs[ix], parsers, out); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		if err := s.printOneObject(obj, parsers, out); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jsonpath.JSONPath, out io.Writer) error {
 | |
| 	columns := make([]string, len(parsers))
 | |
| 	switch u := obj.(type) {
 | |
| 	case *runtime.Unknown:
 | |
| 		if len(u.Raw) > 0 {
 | |
| 			var err error
 | |
| 			if obj, err = runtime.Decode(s.Decoder, u.Raw); err != nil {
 | |
| 				return fmt.Errorf("can't decode object for printing: %v (%s)", err, u.Raw)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for ix := range parsers {
 | |
| 		parser := parsers[ix]
 | |
| 
 | |
| 		var values [][]reflect.Value
 | |
| 		var err error
 | |
| 		if unstructured, ok := obj.(runtime.Unstructured); ok {
 | |
| 			values, err = parser.FindResults(unstructured.UnstructuredContent())
 | |
| 		} else {
 | |
| 			values, err = parser.FindResults(reflect.ValueOf(obj).Elem().Interface())
 | |
| 		}
 | |
| 
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		valueStrings := []string{}
 | |
| 		if len(values) == 0 || len(values[0]) == 0 {
 | |
| 			valueStrings = append(valueStrings, "<none>")
 | |
| 		}
 | |
| 		for arrIx := range values {
 | |
| 			for valIx := range values[arrIx] {
 | |
| 				valueStrings = append(valueStrings, fmt.Sprintf("%v", values[arrIx][valIx].Interface()))
 | |
| 			}
 | |
| 		}
 | |
| 		columns[ix] = strings.Join(valueStrings, ",")
 | |
| 	}
 | |
| 	fmt.Fprintln(out, strings.Join(columns, "\t"))
 | |
| 	return nil
 | |
| }
 |