diff --git a/pkg/kubectl/custom_column_printer.go b/pkg/kubectl/custom_column_printer.go index bbbb16e1444..4d107867713 100644 --- a/pkg/kubectl/custom_column_printer.go +++ b/pkg/kubectl/custom_column_printer.go @@ -17,6 +17,8 @@ limitations under the License. package kubectl import ( + "bufio" + "bytes" "fmt" "io" "reflect" @@ -35,6 +37,95 @@ const ( 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
: 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
:", 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 type Column struct { // 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")) return nil } + +func (s *CustomColumnsPrinter) HandledResources() []string { + return []string{} +} diff --git a/pkg/kubectl/custom_column_printer_test.go b/pkg/kubectl/custom_column_printer_test.go index 62c3aea9dc6..245e1111ee8 100644 --- a/pkg/kubectl/custom_column_printer_test.go +++ b/pkg/kubectl/custom_column_printer_test.go @@ -18,12 +18,177 @@ package kubectl import ( "bytes" + "reflect" "testing" "k8s.io/kubernetes/pkg/api/v1" "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) { tests := []struct { columns []Column diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index fc9ea6b58dd..e7b044348c6 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "io/ioutil" + "os" "reflect" "sort" "strings" @@ -98,6 +99,19 @@ func GetPrinter(format, formatArgument string) (ResourcePrinter, bool, error) { printer, err = NewJSONPathPrinter(string(data)) if err != nil { 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": fallthrough