mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Enable custom columns printing in kubectl
This commit is contained in:
parent
1313e3b14e
commit
b485821bb1
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
package kubectl
|
package kubectl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -35,6 +37,95 @@ const (
|
|||||||
flags = 0
|
flags = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MassageJSONPath attempts to be flexible with JSONPath expressions, it accepts:
|
||||||
|
// * metadata.name (no leading '.' or curly brances '{...}'
|
||||||
|
// * {metadata.name} (no leading '.')
|
||||||
|
// * .metadata.name (no curly braces '{...}')
|
||||||
|
// * {.metadata.name} (complete expression)
|
||||||
|
// And transforms them all into a valid jsonpat expression:
|
||||||
|
// {.metadata.name}
|
||||||
|
func massageJSONPath(pathExpression string) string {
|
||||||
|
if len(pathExpression) == 0 {
|
||||||
|
return pathExpression
|
||||||
|
}
|
||||||
|
if pathExpression[0] != '{' {
|
||||||
|
if pathExpression[0] == '.' {
|
||||||
|
return fmt.Sprintf("{%s}", pathExpression)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("{.%s}", pathExpression)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pathExpression[1] == '.' {
|
||||||
|
return pathExpression
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("{.%s}", pathExpression[1:len(pathExpression)-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (*CustomColumnsPrinter, error) {
|
||||||
|
parts := strings.Split(spec, ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return nil, fmt.Errorf("custom-columns format specified but no custom columns given")
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
columns[ix] = Column{Header: colSpec[0], FieldSpec: massageJSONPath(colSpec[1])}
|
||||||
|
}
|
||||||
|
return &CustomColumnsPrinter{Columns: columns}, 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) (*CustomColumnsPrinter, error) {
|
||||||
|
scanner := bufio.NewScanner(templateReader)
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return nil, fmt.Errorf("invalid template, missing header line")
|
||||||
|
}
|
||||||
|
headers := splitOnWhitespace(scanner.Text())
|
||||||
|
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return nil, fmt.Errorf("invalid template, missing spec line")
|
||||||
|
}
|
||||||
|
specs := splitOnWhitespace(scanner.Text())
|
||||||
|
fmt.Printf("SPECS: %v", specs)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
columns[ix] = Column{
|
||||||
|
Header: headers[ix],
|
||||||
|
FieldSpec: massageJSONPath(specs[ix]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &CustomColumnsPrinter{Columns: columns}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Column represents a user specified column
|
// Column represents a user specified column
|
||||||
type Column struct {
|
type Column struct {
|
||||||
// The header to print above the column, general style is ALL_CAPS
|
// The header to print above the column, general style is ALL_CAPS
|
||||||
@ -105,3 +196,7 @@ func (s *CustomColumnsPrinter) printOneObject(obj runtime.Object, parsers []*jso
|
|||||||
fmt.Fprintln(out, strings.Join(columns, "\t"))
|
fmt.Fprintln(out, strings.Join(columns, "\t"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *CustomColumnsPrinter) HandledResources() []string {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
@ -18,12 +18,177 @@ package kubectl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/api/v1"
|
"k8s.io/kubernetes/pkg/api/v1"
|
||||||
"k8s.io/kubernetes/pkg/runtime"
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMassageJSONPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expectedOutput string
|
||||||
|
}{
|
||||||
|
{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: ""},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
output := massageJSONPath(test.input)
|
||||||
|
if output != test.expectedOutput {
|
||||||
|
t.Errorf("expected: %s, saw: %s", test.expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewColumnPrinterFromSpec(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: "NAME:metadata.name,API_VERSION:apiVersion",
|
||||||
|
name: "ok",
|
||||||
|
expectedColumns: []Column{
|
||||||
|
{
|
||||||
|
Header: "NAME",
|
||||||
|
FieldSpec: "{.metadata.name}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "API_VERSION",
|
||||||
|
FieldSpec: "{.apiVersion}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
printer, err := NewCustomColumnsPrinterFromSpec(test.spec)
|
||||||
|
if test.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("[%s] unexpected non-error", test.name)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !test.expectErr && err != nil {
|
||||||
|
t.Errorf("[%s] unexpected error: %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(test.expectedColumns, printer.Columns) {
|
||||||
|
t.Errorf("[%s]\nexpected:\n%v\nsaw:\n%v\n", test.name, test.expectedColumns, printer.Columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
reader := bytes.NewBufferString(test.spec)
|
||||||
|
printer, err := NewCustomColumnsPrinterFromTemplate(reader)
|
||||||
|
if test.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("[%s] unexpected non-error", test.name)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !test.expectErr && err != nil {
|
||||||
|
t.Errorf("[%s] unexpected error: %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestColumnPrint(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
columns []Column
|
columns []Column
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -99,6 +100,19 @@ func GetPrinter(format, formatArgument string) (ResourcePrinter, bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("error parsing template %s, %v\n", string(data), err)
|
return nil, false, fmt.Errorf("error parsing template %s, %v\n", string(data), err)
|
||||||
}
|
}
|
||||||
|
case "custom-columns":
|
||||||
|
var err error
|
||||||
|
if printer, err = NewCustomColumnsPrinterFromSpec(formatArgument); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
case "custom-columns-file":
|
||||||
|
file, err := os.Open(formatArgument)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, fmt.Errorf("error reading template %s, %v\n", formatArgument, err)
|
||||||
|
}
|
||||||
|
if printer, err = NewCustomColumnsPrinterFromTemplate(file); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
case "wide":
|
case "wide":
|
||||||
fallthrough
|
fallthrough
|
||||||
case "":
|
case "":
|
||||||
|
Loading…
Reference in New Issue
Block a user