kubectl: move custom columns printers and flags

This commit is contained in:
Sean Sullivan
2018-11-01 15:45:38 -07:00
parent 3bcbc5da79
commit e0b712d428
11 changed files with 62 additions and 66 deletions

View File

@@ -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",

View 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
}

View 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: "",
}
}

View 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())
}
})
}
}

View 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())
}
}

View File

@@ -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),
}

View File

@@ -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(),
}
}

View 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())
}

View 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)
}
})
}
}