mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-11-13 06:04:02 +00:00
kubectl: move custom columns printers and flags
This commit is contained in:
@@ -17,19 +17,22 @@ filegroup(
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"customcolumn.go",
|
||||
"customcolumn_flags.go",
|
||||
"get.go",
|
||||
"get_flags.go",
|
||||
"humanreadable_flags.go",
|
||||
"sorter.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubectl/cmd/get",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/api/legacyscheme:go_default_library",
|
||||
"//pkg/kubectl:go_default_library",
|
||||
"//pkg/kubectl/cmd/util:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//pkg/kubectl/scheme:go_default_library",
|
||||
"//pkg/kubectl/util/i18n:go_default_library",
|
||||
"//pkg/kubectl/util/printers:go_default_library",
|
||||
"//pkg/kubectl/util/templates:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
"//pkg/printers/internalversion:go_default_library",
|
||||
@@ -46,19 +49,26 @@ go_library(
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
|
||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/printers:go_default_library",
|
||||
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/rest:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/watch:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/integer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/jsonpath:go_default_library",
|
||||
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
"//vendor/vbom.ml/util/sortorder:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"customcolumn_flags_test.go",
|
||||
"customcolumn_test.go",
|
||||
"get_test.go",
|
||||
"humanreadable_flags_test.go",
|
||||
"sorter_test.go",
|
||||
],
|
||||
data = [
|
||||
"//api/openapi-spec:swagger-spec",
|
||||
@@ -72,13 +82,16 @@ go_test(
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi/testing:go_default_library",
|
||||
"//pkg/kubectl/scheme:go_default_library",
|
||||
"//pkg/kubectl/util/printers:go_default_library",
|
||||
"//staging/src/k8s.io/api/apps/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/batch/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/batch/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
|
||||
243
pkg/kubectl/cmd/get/customcolumn.go
Normal file
243
pkg/kubectl/cmd/get/customcolumn.go
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
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"
|
||||
"text/tabwriter"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/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.Split(parts[ix], ":")
|
||||
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
|
||||
}
|
||||
111
pkg/kubectl/cmd/get/customcolumn_flags.go
Normal file
111
pkg/kubectl/cmd/get/customcolumn_flags.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
||||
)
|
||||
|
||||
var columnsFormats = map[string]bool{
|
||||
"custom-columns-file": true,
|
||||
"custom-columns": true,
|
||||
}
|
||||
|
||||
// CustomColumnsPrintFlags provides default flags necessary for printing
|
||||
// custom resource columns from an inline-template or file.
|
||||
type CustomColumnsPrintFlags struct {
|
||||
NoHeaders bool
|
||||
TemplateArgument string
|
||||
}
|
||||
|
||||
func (f *CustomColumnsPrintFlags) AllowedFormats() []string {
|
||||
formats := make([]string, 0, len(columnsFormats))
|
||||
for format := range columnsFormats {
|
||||
formats = append(formats, format)
|
||||
}
|
||||
return formats
|
||||
}
|
||||
|
||||
// ToPrinter receives an templateFormat and returns a printer capable of
|
||||
// handling custom-column printing.
|
||||
// Returns false if the specified templateFormat does not match a supported format.
|
||||
// Supported format types can be found in pkg/printers/printers.go
|
||||
func (f *CustomColumnsPrintFlags) ToPrinter(templateFormat string) (printers.ResourcePrinter, error) {
|
||||
if len(templateFormat) == 0 {
|
||||
return nil, genericclioptions.NoCompatiblePrinterError{}
|
||||
}
|
||||
|
||||
templateValue := ""
|
||||
|
||||
if len(f.TemplateArgument) == 0 {
|
||||
for format := range columnsFormats {
|
||||
format = format + "="
|
||||
if strings.HasPrefix(templateFormat, format) {
|
||||
templateValue = templateFormat[len(format):]
|
||||
templateFormat = format[:len(format)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
templateValue = f.TemplateArgument
|
||||
}
|
||||
|
||||
if _, supportedFormat := columnsFormats[templateFormat]; !supportedFormat {
|
||||
return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &templateFormat, AllowedFormats: f.AllowedFormats()}
|
||||
}
|
||||
|
||||
if len(templateValue) == 0 {
|
||||
return nil, fmt.Errorf("custom-columns format specified but no custom columns given")
|
||||
}
|
||||
|
||||
// UniversalDecoder call must specify parameter versions; otherwise it will decode to internal versions.
|
||||
decoder := scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
if templateFormat == "custom-columns-file" {
|
||||
file, err := os.Open(templateValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading template %s, %v\n", templateValue, err)
|
||||
}
|
||||
defer file.Close()
|
||||
p, err := NewCustomColumnsPrinterFromTemplate(file, decoder)
|
||||
return p, err
|
||||
}
|
||||
|
||||
return NewCustomColumnsPrinterFromSpec(templateValue, decoder, f.NoHeaders)
|
||||
}
|
||||
|
||||
// AddFlags receives a *cobra.Command reference and binds
|
||||
// flags related to custom-columns printing
|
||||
func (f *CustomColumnsPrintFlags) AddFlags(c *cobra.Command) {}
|
||||
|
||||
// NewCustomColumnsPrintFlags returns flags associated with
|
||||
// custom-column printing, with default values set.
|
||||
// NoHeaders and TemplateArgument should be set by callers.
|
||||
func NewCustomColumnsPrintFlags() *CustomColumnsPrintFlags {
|
||||
return &CustomColumnsPrintFlags{
|
||||
NoHeaders: false,
|
||||
TemplateArgument: "",
|
||||
}
|
||||
}
|
||||
139
pkg/kubectl/cmd/get/customcolumn_flags_test.go
Normal file
139
pkg/kubectl/cmd/get/customcolumn_flags_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
Copyright 2018 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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
)
|
||||
|
||||
func TestPrinterSupportsExpectedCustomColumnFormats(t *testing.T) {
|
||||
testObject := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}
|
||||
|
||||
customColumnsFile, err := ioutil.TempFile("", "printers_jsonpath_flags")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer func(tempFile *os.File) {
|
||||
tempFile.Close()
|
||||
os.Remove(tempFile.Name())
|
||||
}(customColumnsFile)
|
||||
|
||||
fmt.Fprintf(customColumnsFile, "NAME\n.metadata.name")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
outputFormat string
|
||||
templateArg string
|
||||
expectedError string
|
||||
expectedParseError string
|
||||
expectedOutput string
|
||||
expectNoMatch bool
|
||||
}{
|
||||
{
|
||||
name: "valid output format also containing the custom-columns argument succeeds",
|
||||
outputFormat: "custom-columns=NAME:.metadata.name",
|
||||
expectedOutput: "foo",
|
||||
},
|
||||
{
|
||||
name: "valid output format and no --template argument results in an error",
|
||||
outputFormat: "custom-columns",
|
||||
expectedError: "custom-columns format specified but no custom columns given",
|
||||
},
|
||||
{
|
||||
name: "valid output format and --template argument succeeds",
|
||||
outputFormat: "custom-columns",
|
||||
templateArg: "NAME:.metadata.name",
|
||||
expectedOutput: "foo",
|
||||
},
|
||||
{
|
||||
name: "custom-columns template file should match, and successfully return correct value",
|
||||
outputFormat: "custom-columns-file",
|
||||
templateArg: customColumnsFile.Name(),
|
||||
expectedOutput: "foo",
|
||||
},
|
||||
{
|
||||
name: "valid output format and invalid --template argument results in a parsing error from the printer",
|
||||
outputFormat: "custom-columns",
|
||||
templateArg: "invalid",
|
||||
expectedError: "unexpected custom-columns spec: invalid, expected <header>:<json-path-expr>",
|
||||
},
|
||||
{
|
||||
name: "no printer is matched on an invalid outputFormat",
|
||||
outputFormat: "invalid",
|
||||
expectNoMatch: true,
|
||||
},
|
||||
{
|
||||
name: "custom-columns printer should not match on any other format supported by another printer",
|
||||
outputFormat: "go-template",
|
||||
expectNoMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
printFlags := CustomColumnsPrintFlags{
|
||||
TemplateArgument: tc.templateArg,
|
||||
}
|
||||
|
||||
p, err := printFlags.ToPrinter(tc.outputFormat)
|
||||
if tc.expectNoMatch {
|
||||
if !genericclioptions.IsNoCompatiblePrinterError(err) {
|
||||
t.Fatalf("expected no printer matches for output format %q", tc.outputFormat)
|
||||
}
|
||||
return
|
||||
}
|
||||
if genericclioptions.IsNoCompatiblePrinterError(err) {
|
||||
t.Fatalf("expected to match template printer for output format %q", tc.outputFormat)
|
||||
}
|
||||
|
||||
if len(tc.expectedError) > 0 {
|
||||
if err == nil || !strings.Contains(err.Error(), tc.expectedError) {
|
||||
t.Errorf("expecting error %q, got %v", tc.expectedError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer([]byte{})
|
||||
err = p.PrintObj(testObject, out)
|
||||
if len(tc.expectedParseError) > 0 {
|
||||
if err == nil || !strings.Contains(err.Error(), tc.expectedParseError) {
|
||||
t.Errorf("expecting error %q, got %v", tc.expectedError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(out.String(), tc.expectedOutput) {
|
||||
t.Errorf("unexpected output: expecting %q, got %q", tc.expectedOutput, out.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
375
pkg/kubectl/cmd/get/customcolumn_test.go
Normal file
375
pkg/kubectl/cmd/get/customcolumn_test.go
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/printers"
|
||||
)
|
||||
|
||||
// UniversalDecoder call must specify parameter versions; otherwise it will decode to internal versions.
|
||||
var decoder = scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
|
||||
func TestMassageJSONPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedOutput string
|
||||
expectErr bool
|
||||
}{
|
||||
{input: "foo.bar", expectedOutput: "{.foo.bar}"},
|
||||
{input: "{foo.bar}", expectedOutput: "{.foo.bar}"},
|
||||
{input: ".foo.bar", expectedOutput: "{.foo.bar}"},
|
||||
{input: "{.foo.bar}", expectedOutput: "{.foo.bar}"},
|
||||
{input: "", expectedOutput: ""},
|
||||
{input: "{foo.bar", expectErr: true},
|
||||
{input: "foo.bar}", expectErr: true},
|
||||
{input: "{foo.bar}}", expectErr: true},
|
||||
{input: "{{foo.bar}", expectErr: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
output, err := RelaxedJSONPathExpression(test.input)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Error("unexpected non-error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if output != test.expectedOutput {
|
||||
t.Errorf("input: %s, expected: %s, saw: %s", test.input, test.expectedOutput, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewColumnPrinterFromSpec(t *testing.T) {
|
||||
tests := []struct {
|
||||
spec string
|
||||
expectedColumns []Column
|
||||
expectErr bool
|
||||
name string
|
||||
noHeaders bool
|
||||
}{
|
||||
{
|
||||
spec: "",
|
||||
expectErr: true,
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
spec: "invalid",
|
||||
expectErr: true,
|
||||
name: "invalid1",
|
||||
},
|
||||
{
|
||||
spec: "invalid=foobar",
|
||||
expectErr: true,
|
||||
name: "invalid2",
|
||||
},
|
||||
{
|
||||
spec: "invalid,foobar:blah",
|
||||
expectErr: true,
|
||||
name: "invalid3",
|
||||
},
|
||||
{
|
||||
spec: "NAME:metadata.name,API_VERSION:apiVersion",
|
||||
name: "ok",
|
||||
expectedColumns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
Header: "API_VERSION",
|
||||
FieldSpec: "{.apiVersion}",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: "API_VERSION:apiVersion",
|
||||
name: "no-headers",
|
||||
noHeaders: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
printer, err := NewCustomColumnsPrinterFromSpec(test.spec, decoder, test.noHeaders)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("[%s] unexpected non-error", test.name)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("[%s] unexpected error: %v", test.name, err)
|
||||
return
|
||||
}
|
||||
if test.noHeaders {
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
printer.PrintObj(&corev1.Pod{}, buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("An error occurred printing Pod: %#v", err)
|
||||
}
|
||||
|
||||
if contains(strings.Fields(buffer.String()), "API_VERSION") {
|
||||
t.Errorf("unexpected header API_VERSION")
|
||||
}
|
||||
|
||||
} else if !reflect.DeepEqual(test.expectedColumns, printer.Columns) {
|
||||
t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v\n", test.name, test.expectedColumns, printer.Columns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(arr []string, s string) bool {
|
||||
for i := range arr {
|
||||
if arr[i] == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const exampleTemplateOne = `NAME API_VERSION
|
||||
{metadata.name} {apiVersion}`
|
||||
|
||||
const exampleTemplateTwo = `NAME API_VERSION
|
||||
{metadata.name} {apiVersion}`
|
||||
|
||||
func TestNewColumnPrinterFromTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
spec string
|
||||
expectedColumns []Column
|
||||
expectErr bool
|
||||
name string
|
||||
}{
|
||||
{
|
||||
spec: "",
|
||||
expectErr: true,
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
spec: "invalid",
|
||||
expectErr: true,
|
||||
name: "invalid1",
|
||||
},
|
||||
{
|
||||
spec: "invalid=foobar",
|
||||
expectErr: true,
|
||||
name: "invalid2",
|
||||
},
|
||||
{
|
||||
spec: "invalid,foobar:blah",
|
||||
expectErr: true,
|
||||
name: "invalid3",
|
||||
},
|
||||
{
|
||||
spec: exampleTemplateOne,
|
||||
name: "ok",
|
||||
expectedColumns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
Header: "API_VERSION",
|
||||
FieldSpec: "{.apiVersion}",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
spec: exampleTemplateTwo,
|
||||
name: "ok-2",
|
||||
expectedColumns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
Header: "API_VERSION",
|
||||
FieldSpec: "{.apiVersion}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader := bytes.NewBufferString(test.spec)
|
||||
printer, err := NewCustomColumnsPrinterFromTemplate(reader, decoder)
|
||||
if test.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("[%s] unexpected non-error", test.name)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !test.expectErr && err != nil {
|
||||
t.Errorf("[%s] unexpected error: %v", test.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(test.expectedColumns, printer.Columns) {
|
||||
t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v\n", test.name, test.expectedColumns, printer.Columns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnPrint(t *testing.T) {
|
||||
tests := []struct {
|
||||
columns []Column
|
||||
obj runtime.Object
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
columns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
},
|
||||
obj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
expectedOutput: `NAME
|
||||
foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
columns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
},
|
||||
obj: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "bar"}},
|
||||
},
|
||||
},
|
||||
expectedOutput: `NAME
|
||||
foo
|
||||
bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
columns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
Header: "API_VERSION",
|
||||
FieldSpec: "{.apiVersion}",
|
||||
},
|
||||
},
|
||||
obj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, TypeMeta: metav1.TypeMeta{APIVersion: "baz"}},
|
||||
expectedOutput: `NAME API_VERSION
|
||||
foo baz
|
||||
`,
|
||||
},
|
||||
{
|
||||
columns: []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
Header: "API_VERSION",
|
||||
FieldSpec: "{.apiVersion}",
|
||||
},
|
||||
{
|
||||
Header: "NOT_FOUND",
|
||||
FieldSpec: "{.notFound}",
|
||||
},
|
||||
},
|
||||
obj: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, TypeMeta: metav1.TypeMeta{APIVersion: "baz"}},
|
||||
expectedOutput: `NAME API_VERSION NOT_FOUND
|
||||
foo baz <none>
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.expectedOutput, func(t *testing.T) {
|
||||
printer := &CustomColumnsPrinter{
|
||||
Columns: test.columns,
|
||||
Decoder: decoder,
|
||||
}
|
||||
buffer := &bytes.Buffer{}
|
||||
if err := printer.PrintObj(test.obj, buffer); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if buffer.String() != test.expectedOutput {
|
||||
t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", test.expectedOutput, buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// this mimics how resource/get.go calls the customcolumn printer
|
||||
func TestIndividualPrintObjOnExistingTabWriter(t *testing.T) {
|
||||
columns := []Column{
|
||||
{
|
||||
Header: "NAME",
|
||||
FieldSpec: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
Header: "LONG COLUMN NAME", // name is longer than all values of label1
|
||||
FieldSpec: "{.metadata.labels.label1}",
|
||||
},
|
||||
{
|
||||
Header: "LABEL 2",
|
||||
FieldSpec: "{.metadata.labels.label2}",
|
||||
},
|
||||
}
|
||||
objects := []*corev1.Pod{
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{"label1": "foo", "label2": "foo"}}},
|
||||
{ObjectMeta: metav1.ObjectMeta{Name: "bar", Labels: map[string]string{"label1": "bar", "label2": "bar"}}},
|
||||
}
|
||||
expectedOutput := `NAME LONG COLUMN NAME LABEL 2
|
||||
foo foo foo
|
||||
bar bar bar
|
||||
`
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
tabWriter := printers.GetNewTabWriter(buffer)
|
||||
printer := &CustomColumnsPrinter{
|
||||
Columns: columns,
|
||||
Decoder: decoder,
|
||||
}
|
||||
for _, obj := range objects {
|
||||
if err := printer.PrintObj(obj, tabWriter); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
tabWriter.Flush()
|
||||
if buffer.String() != expectedOutput {
|
||||
t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", expectedOutput, buffer.String())
|
||||
}
|
||||
}
|
||||
@@ -38,15 +38,15 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/resource"
|
||||
"k8s.io/client-go/rest"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/kubectl"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||
utilprinters "k8s.io/kubernetes/pkg/kubectl/util/printers"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/templates"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
"k8s.io/kubernetes/pkg/util/interrupt"
|
||||
)
|
||||
|
||||
@@ -331,7 +331,7 @@ func (r *RuntimeSorter) Sort() error {
|
||||
case *metav1beta1.Table:
|
||||
includesTable = true
|
||||
|
||||
if err := kubectl.NewTableSorter(t, r.field).Sort(); err != nil {
|
||||
if err := NewTableSorter(t, r.field).Sort(); err != nil {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
@@ -354,7 +354,7 @@ func (r *RuntimeSorter) Sort() error {
|
||||
// if not dealing with a Table response from the server, assume
|
||||
// all objects are runtime.Object as usual, and sort using old method.
|
||||
var err error
|
||||
if r.positioner, err = kubectl.SortObjects(r.decoder, r.objects, r.field); err != nil {
|
||||
if r.positioner, err = SortObjects(r.decoder, r.objects, r.field); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -374,7 +374,7 @@ func (r *RuntimeSorter) WithDecoder(decoder runtime.Decoder) *RuntimeSorter {
|
||||
}
|
||||
|
||||
func NewRuntimeSorter(objects []runtime.Object, sortBy string) *RuntimeSorter {
|
||||
parsedField, err := printers.RelaxedJSONPathExpression(sortBy)
|
||||
parsedField, err := RelaxedJSONPathExpression(sortBy)
|
||||
if err != nil {
|
||||
parsedField = sortBy
|
||||
}
|
||||
@@ -495,7 +495,7 @@ func (o *GetOptions) Run(f cmdutil.Factory, cmd *cobra.Command, args []string) e
|
||||
var printer printers.ResourcePrinter
|
||||
var lastMapping *meta.RESTMapping
|
||||
nonEmptyObjCount := 0
|
||||
w := printers.GetNewTabWriter(o.Out)
|
||||
w := utilprinters.GetNewTabWriter(o.Out)
|
||||
for ix := range objs {
|
||||
var mapping *meta.RESTMapping
|
||||
var info *resource.Info
|
||||
@@ -645,7 +645,7 @@ func (o *GetOptions) watch(f cmdutil.Factory, cmd *cobra.Command, args []string)
|
||||
// print the current object
|
||||
if !o.WatchOnly {
|
||||
var objsToPrint []runtime.Object
|
||||
writer := printers.GetNewTabWriter(o.Out)
|
||||
writer := utilprinters.GetNewTabWriter(o.Out)
|
||||
|
||||
if isList {
|
||||
objsToPrint, _ = meta.ExtractList(obj)
|
||||
@@ -852,7 +852,7 @@ func cmdSpecifiesOutputFmt(cmd *cobra.Command) bool {
|
||||
|
||||
func maybeWrapSortingPrinter(printer printers.ResourcePrinter, sortBy string) printers.ResourcePrinter {
|
||||
if len(sortBy) != 0 {
|
||||
return &kubectl.SortingPrinter{
|
||||
return &SortingPrinter{
|
||||
Delegate: printer,
|
||||
SortField: fmt.Sprintf("%s", sortBy),
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
)
|
||||
|
||||
// PrintFlags composes common printer flag structs
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
type PrintFlags struct {
|
||||
JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags
|
||||
NamePrintFlags *genericclioptions.NamePrintFlags
|
||||
CustomColumnsFlags *printers.CustomColumnsPrintFlags
|
||||
CustomColumnsFlags *CustomColumnsPrintFlags
|
||||
HumanReadableFlags *HumanPrintFlags
|
||||
TemplateFlags *genericclioptions.KubeTemplatePrintFlags
|
||||
|
||||
@@ -185,6 +185,6 @@ func NewGetPrintFlags() *PrintFlags {
|
||||
TemplateFlags: genericclioptions.NewKubeTemplatePrintFlags(),
|
||||
|
||||
HumanReadableFlags: NewHumanPrintFlags(),
|
||||
CustomColumnsFlags: printers.NewCustomColumnsPrintFlags(),
|
||||
CustomColumnsFlags: NewCustomColumnsPrintFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
373
pkg/kubectl/cmd/get/sorter.go
Normal file
373
pkg/kubectl/cmd/get/sorter.go
Normal file
@@ -0,0 +1,373 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
|
||||
"k8s.io/client-go/util/integer"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
// SortingPrinter sorts list types before delegating to another printer.
|
||||
// Non-list types are simply passed through
|
||||
type SortingPrinter struct {
|
||||
SortField string
|
||||
Delegate printers.ResourcePrinter
|
||||
Decoder runtime.Decoder
|
||||
}
|
||||
|
||||
func (s *SortingPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
|
||||
if !meta.IsListType(obj) {
|
||||
return s.Delegate.PrintObj(obj, out)
|
||||
}
|
||||
|
||||
if err := s.sortObj(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Delegate.PrintObj(obj, out)
|
||||
}
|
||||
|
||||
func (s *SortingPrinter) sortObj(obj runtime.Object) error {
|
||||
objs, err := meta.ExtractList(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(objs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sorter, err := SortObjects(s.Decoder, objs, s.SortField)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch list := obj.(type) {
|
||||
case *corev1.List:
|
||||
outputList := make([]runtime.RawExtension, len(objs))
|
||||
for ix := range objs {
|
||||
outputList[ix] = list.Items[sorter.OriginalPosition(ix)]
|
||||
}
|
||||
list.Items = outputList
|
||||
return nil
|
||||
}
|
||||
return meta.SetList(obj, objs)
|
||||
}
|
||||
|
||||
func SortObjects(decoder runtime.Decoder, objs []runtime.Object, fieldInput string) (*RuntimeSort, error) {
|
||||
for ix := range objs {
|
||||
item := objs[ix]
|
||||
switch u := item.(type) {
|
||||
case *runtime.Unknown:
|
||||
var err error
|
||||
// decode runtime.Unknown to runtime.Unstructured for sorting.
|
||||
// we don't actually want the internal versions of known types.
|
||||
if objs[ix], _, err = decoder.Decode(u.Raw, nil, &unstructured.Unstructured{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
field, err := RelaxedJSONPathExpression(fieldInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parser := jsonpath.New("sorting").AllowMissingKeys(true)
|
||||
if err := parser.Parse(field); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't do any model validation here, so we traverse all objects to be sorted
|
||||
// and, if the field is valid to at least one of them, we consider it to be a
|
||||
// valid field; otherwise error out.
|
||||
// Note that this requires empty fields to be considered later, when sorting.
|
||||
var fieldFoundOnce bool
|
||||
for _, obj := range objs {
|
||||
values, err := findJSONPathResults(parser, obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(values) > 0 && len(values[0]) > 0 {
|
||||
fieldFoundOnce = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !fieldFoundOnce {
|
||||
return nil, fmt.Errorf("couldn't find any field with path %q in the list of objects", field)
|
||||
}
|
||||
|
||||
sorter := NewRuntimeSort(field, objs)
|
||||
sort.Sort(sorter)
|
||||
return sorter, nil
|
||||
}
|
||||
|
||||
// RuntimeSort is an implementation of the golang sort interface that knows how to sort
|
||||
// lists of runtime.Object
|
||||
type RuntimeSort struct {
|
||||
field string
|
||||
objs []runtime.Object
|
||||
origPosition []int
|
||||
}
|
||||
|
||||
func NewRuntimeSort(field string, objs []runtime.Object) *RuntimeSort {
|
||||
sorter := &RuntimeSort{field: field, objs: objs, origPosition: make([]int, len(objs))}
|
||||
for ix := range objs {
|
||||
sorter.origPosition[ix] = ix
|
||||
}
|
||||
return sorter
|
||||
}
|
||||
|
||||
func (r *RuntimeSort) Len() int {
|
||||
return len(r.objs)
|
||||
}
|
||||
|
||||
func (r *RuntimeSort) Swap(i, j int) {
|
||||
r.objs[i], r.objs[j] = r.objs[j], r.objs[i]
|
||||
r.origPosition[i], r.origPosition[j] = r.origPosition[j], r.origPosition[i]
|
||||
}
|
||||
|
||||
func isLess(i, j reflect.Value) (bool, error) {
|
||||
switch i.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return i.Int() < j.Int(), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return i.Uint() < j.Uint(), nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return i.Float() < j.Float(), nil
|
||||
case reflect.String:
|
||||
return sortorder.NaturalLess(i.String(), j.String()), nil
|
||||
case reflect.Ptr:
|
||||
return isLess(i.Elem(), j.Elem())
|
||||
case reflect.Struct:
|
||||
// sort metav1.Time
|
||||
in := i.Interface()
|
||||
if t, ok := in.(metav1.Time); ok {
|
||||
time := j.Interface().(metav1.Time)
|
||||
return t.Before(&time), nil
|
||||
}
|
||||
// fallback to the fields comparison
|
||||
for idx := 0; idx < i.NumField(); idx++ {
|
||||
less, err := isLess(i.Field(idx), j.Field(idx))
|
||||
if err != nil || !less {
|
||||
return less, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
case reflect.Array, reflect.Slice:
|
||||
// note: the length of i and j may be different
|
||||
for idx := 0; idx < integer.IntMin(i.Len(), j.Len()); idx++ {
|
||||
less, err := isLess(i.Index(idx), j.Index(idx))
|
||||
if err != nil || !less {
|
||||
return less, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
|
||||
case reflect.Interface:
|
||||
switch itype := i.Interface().(type) {
|
||||
case uint8:
|
||||
if jtype, ok := j.Interface().(uint8); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case uint16:
|
||||
if jtype, ok := j.Interface().(uint16); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case uint32:
|
||||
if jtype, ok := j.Interface().(uint32); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case uint64:
|
||||
if jtype, ok := j.Interface().(uint64); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case int8:
|
||||
if jtype, ok := j.Interface().(int8); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case int16:
|
||||
if jtype, ok := j.Interface().(int16); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case int32:
|
||||
if jtype, ok := j.Interface().(int32); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case int64:
|
||||
if jtype, ok := j.Interface().(int64); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case uint:
|
||||
if jtype, ok := j.Interface().(uint); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case int:
|
||||
if jtype, ok := j.Interface().(int); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case float32:
|
||||
if jtype, ok := j.Interface().(float32); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case float64:
|
||||
if jtype, ok := j.Interface().(float64); ok {
|
||||
return itype < jtype, nil
|
||||
}
|
||||
case string:
|
||||
if jtype, ok := j.Interface().(string); ok {
|
||||
return sortorder.NaturalLess(itype, jtype), nil
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("unsortable type: %T", itype)
|
||||
}
|
||||
return false, fmt.Errorf("unsortable interface: %v", i.Kind())
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unsortable type: %v", i.Kind())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RuntimeSort) Less(i, j int) bool {
|
||||
iObj := r.objs[i]
|
||||
jObj := r.objs[j]
|
||||
|
||||
var iValues [][]reflect.Value
|
||||
var jValues [][]reflect.Value
|
||||
var err error
|
||||
|
||||
parser := jsonpath.New("sorting").AllowMissingKeys(true)
|
||||
err = parser.Parse(r.field)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
iValues, err = findJSONPathResults(parser, iObj)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to get i values for %#v using %s (%#v)", iObj, r.field, err)
|
||||
}
|
||||
|
||||
jValues, err = findJSONPathResults(parser, jObj)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to get j values for %#v using %s (%v)", jObj, r.field, err)
|
||||
}
|
||||
|
||||
if len(iValues) == 0 || len(iValues[0]) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(jValues) == 0 || len(jValues[0]) == 0 {
|
||||
return false
|
||||
}
|
||||
iField := iValues[0][0]
|
||||
jField := jValues[0][0]
|
||||
|
||||
less, err := isLess(iField, jField)
|
||||
if err != nil {
|
||||
klog.Fatalf("Field %s in %T is an unsortable type: %s, err: %v", r.field, iObj, iField.Kind().String(), err)
|
||||
}
|
||||
return less
|
||||
}
|
||||
|
||||
// OriginalPosition returns the starting (original) position of a particular index.
|
||||
// e.g. If OriginalPosition(0) returns 5 than the
|
||||
// the item currently at position 0 was at position 5 in the original unsorted array.
|
||||
func (r *RuntimeSort) OriginalPosition(ix int) int {
|
||||
if ix < 0 || ix > len(r.origPosition) {
|
||||
return -1
|
||||
}
|
||||
return r.origPosition[ix]
|
||||
}
|
||||
|
||||
type TableSorter struct {
|
||||
field string
|
||||
obj *metav1beta1.Table
|
||||
parsedRows [][][]reflect.Value
|
||||
}
|
||||
|
||||
func (t *TableSorter) Len() int {
|
||||
return len(t.obj.Rows)
|
||||
}
|
||||
|
||||
func (t *TableSorter) Swap(i, j int) {
|
||||
t.obj.Rows[i], t.obj.Rows[j] = t.obj.Rows[j], t.obj.Rows[i]
|
||||
}
|
||||
|
||||
func (t *TableSorter) Less(i, j int) bool {
|
||||
iValues := t.parsedRows[i]
|
||||
jValues := t.parsedRows[j]
|
||||
if len(iValues) == 0 || len(iValues[0]) == 0 || len(jValues) == 0 || len(jValues[0]) == 0 {
|
||||
klog.Fatalf("couldn't find any field with path %q in the list of objects", t.field)
|
||||
}
|
||||
|
||||
iField := iValues[0][0]
|
||||
jField := jValues[0][0]
|
||||
|
||||
less, err := isLess(iField, jField)
|
||||
if err != nil {
|
||||
klog.Fatalf("Field %s in %T is an unsortable type: %s, err: %v", t.field, t.parsedRows, iField.Kind().String(), err)
|
||||
}
|
||||
return less
|
||||
}
|
||||
|
||||
func (t *TableSorter) Sort() error {
|
||||
sort.Sort(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTableSorter(table *metav1beta1.Table, field string) *TableSorter {
|
||||
var parsedRows [][][]reflect.Value
|
||||
|
||||
parser := jsonpath.New("sorting").AllowMissingKeys(true)
|
||||
err := parser.Parse(field)
|
||||
if err != nil {
|
||||
klog.Fatalf("sorting error: %v\n", err)
|
||||
}
|
||||
|
||||
for i := range table.Rows {
|
||||
parsedRow, err := findJSONPathResults(parser, table.Rows[i].Object.Object)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to get values for %#v using %s (%#v)", parsedRow, field, err)
|
||||
}
|
||||
parsedRows = append(parsedRows, parsedRow)
|
||||
}
|
||||
|
||||
return &TableSorter{
|
||||
obj: table,
|
||||
field: field,
|
||||
parsedRows: parsedRows,
|
||||
}
|
||||
}
|
||||
func findJSONPathResults(parser *jsonpath.JSONPath, from runtime.Object) ([][]reflect.Value, error) {
|
||||
if unstructuredObj, ok := from.(*unstructured.Unstructured); ok {
|
||||
return parser.FindResults(unstructuredObj.Object)
|
||||
}
|
||||
return parser.FindResults(reflect.ValueOf(from).Elem().Interface())
|
||||
}
|
||||
477
pkg/kubectl/cmd/get/sorter_test.go
Normal file
477
pkg/kubectl/cmd/get/sorter_test.go
Normal file
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
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 (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
||||
)
|
||||
|
||||
func encodeOrDie(obj runtime.Object) []byte {
|
||||
data, err := runtime.Encode(scheme.Codecs.LegacyCodec(corev1.SchemeGroupVersion), obj)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func TestSortingPrinter(t *testing.T) {
|
||||
intPtr := func(val int32) *int32 { return &val }
|
||||
|
||||
a := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
},
|
||||
}
|
||||
|
||||
b := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "b",
|
||||
},
|
||||
}
|
||||
|
||||
c := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "c",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
obj runtime.Object
|
||||
sort runtime.Object
|
||||
field string
|
||||
name string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "in-order-already",
|
||||
obj: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
name: "reverse-order",
|
||||
obj: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "c",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "c",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
name: "random-order-timestamp",
|
||||
obj: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Unix(300, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Unix(100, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Unix(200, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Unix(100, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Unix(200, 0),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
CreationTimestamp: metav1.Unix(300, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.metadata.creationTimestamp}",
|
||||
},
|
||||
{
|
||||
name: "random-order-numbers",
|
||||
obj: &corev1.ReplicationControllerList{
|
||||
Items: []corev1.ReplicationController{
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(5),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(9),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: &corev1.ReplicationControllerList{
|
||||
Items: []corev1.ReplicationController{
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(5),
|
||||
},
|
||||
},
|
||||
{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Replicas: intPtr(9),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.spec.replicas}",
|
||||
},
|
||||
{
|
||||
name: "v1.List in order",
|
||||
obj: &corev1.List{
|
||||
Items: []runtime.RawExtension{
|
||||
{Raw: encodeOrDie(a)},
|
||||
{Raw: encodeOrDie(b)},
|
||||
{Raw: encodeOrDie(c)},
|
||||
},
|
||||
},
|
||||
sort: &corev1.List{
|
||||
Items: []runtime.RawExtension{
|
||||
{Raw: encodeOrDie(a)},
|
||||
{Raw: encodeOrDie(b)},
|
||||
{Raw: encodeOrDie(c)},
|
||||
},
|
||||
},
|
||||
field: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
name: "v1.List in reverse",
|
||||
obj: &corev1.List{
|
||||
Items: []runtime.RawExtension{
|
||||
{Raw: encodeOrDie(c)},
|
||||
{Raw: encodeOrDie(b)},
|
||||
{Raw: encodeOrDie(a)},
|
||||
},
|
||||
},
|
||||
sort: &corev1.List{
|
||||
Items: []runtime.RawExtension{
|
||||
{Raw: encodeOrDie(a)},
|
||||
{Raw: encodeOrDie(b)},
|
||||
{Raw: encodeOrDie(c)},
|
||||
},
|
||||
},
|
||||
field: "{.metadata.name}",
|
||||
},
|
||||
{
|
||||
name: "some-missing-fields",
|
||||
obj: &unstructured.UnstructuredList{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "List",
|
||||
"apiVersion": "v1",
|
||||
},
|
||||
Items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{
|
||||
"availableReplicas": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{
|
||||
"availableReplicas": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: &unstructured.UnstructuredList{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "List",
|
||||
"apiVersion": "v1",
|
||||
},
|
||||
Items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{
|
||||
"availableReplicas": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{
|
||||
"availableReplicas": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.status.availableReplicas}",
|
||||
},
|
||||
{
|
||||
name: "all-missing-fields",
|
||||
obj: &unstructured.UnstructuredList{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "List",
|
||||
"apiVersion": "v1",
|
||||
},
|
||||
Items: []unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{
|
||||
"replicas": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "ReplicationController",
|
||||
"apiVersion": "v1",
|
||||
"status": map[string]interface{}{
|
||||
"replicas": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.status.availableReplicas}",
|
||||
expectedErr: "couldn't find any field with path \"{.status.availableReplicas}\" in the list of objects",
|
||||
},
|
||||
{
|
||||
name: "model-invalid-fields",
|
||||
obj: &corev1.ReplicationControllerList{
|
||||
Items: []corev1.ReplicationController{
|
||||
{
|
||||
Status: corev1.ReplicationControllerStatus{},
|
||||
},
|
||||
{
|
||||
Status: corev1.ReplicationControllerStatus{},
|
||||
},
|
||||
{
|
||||
Status: corev1.ReplicationControllerStatus{},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: "{.invalid}",
|
||||
expectedErr: "couldn't find any field with path \"{.invalid}\" in the list of objects",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sort := &SortingPrinter{SortField: tt.field, Decoder: scheme.Codecs.UniversalDecoder()}
|
||||
err := sort.sortObj(tt.obj)
|
||||
if err != nil {
|
||||
if len(tt.expectedErr) > 0 {
|
||||
if strings.Contains(err.Error(), tt.expectedErr) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("%s: expected error containing: %q, got: \"%v\"", tt.name, tt.expectedErr, err)
|
||||
}
|
||||
t.Fatalf("%s: unexpected error: %v", tt.name, err)
|
||||
}
|
||||
if len(tt.expectedErr) > 0 {
|
||||
t.Fatalf("%s: expected error containing: %q, got none", tt.name, tt.expectedErr)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.obj, tt.sort) {
|
||||
t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v", tt.name, tt.sort, tt.obj)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeSortLess(t *testing.T) {
|
||||
var testobj runtime.Object
|
||||
|
||||
testobj = &corev1.PodList{
|
||||
Items: []corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "c",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testobjs, err := meta.ExtractList(testobj)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractList testobj got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
testfield := "{.metadata.name}"
|
||||
testruntimeSort := NewRuntimeSort(testfield, testobjs)
|
||||
tests := []struct {
|
||||
name string
|
||||
runtimeSort *RuntimeSort
|
||||
i int
|
||||
j int
|
||||
expectResult bool
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "test less true",
|
||||
runtimeSort: testruntimeSort,
|
||||
i: 0,
|
||||
j: 1,
|
||||
expectResult: true,
|
||||
},
|
||||
{
|
||||
name: "test less false",
|
||||
runtimeSort: testruntimeSort,
|
||||
i: 1,
|
||||
j: 2,
|
||||
expectResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := test.runtimeSort.Less(test.i, test.j)
|
||||
if result != test.expectResult {
|
||||
t.Errorf("case[%d]:%s Expected result: %v, Got result: %v", i, test.name, test.expectResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user