Merge pull request #13545 from brendandburns/kubectl

Auto commit by PR queue bot
This commit is contained in:
k8s-merge-robot 2015-09-10 17:23:11 -07:00
commit e46c940cae
3 changed files with 300 additions and 0 deletions

View File

@ -17,9 +17,12 @@ limitations under the License.
package kubectl
import (
"bufio"
"bytes"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"text/tabwriter"
@ -35,6 +38,105 @@ const (
flags = 0
)
var jsonRegexp = regexp.MustCompile("^\\{\\.?([^{}]+)\\}$|^\\.?([^{}]+)$")
// 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, 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) (*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 := massageJSONPath(colSpec[1])
if err != nil {
return nil, err
}
columns[ix] = Column{Header: colSpec[0], FieldSpec: spec}
}
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. 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 := massageJSONPath(specs[ix])
if err != nil {
return nil, err
}
columns[ix] = Column{
Header: headers[ix],
FieldSpec: spec,
}
}
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 +207,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{}
}

View File

@ -18,12 +18,192 @@ 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
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 {
output, err := massageJSONPath(test.input)
if err != nil && !test.expectErr {
t.Errorf("unexpected error: %v", err)
continue
}
if test.expectErr {
if err == nil {
t.Error("unexpected non-error")
}
continue
}
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
}{
{
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

View File

@ -23,6 +23,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"sort"
"strings"
@ -100,6 +101,19 @@ func GetPrinter(format, formatArgument string) (ResourcePrinter, bool, error) {
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
case "":