mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 10:18:13 +00:00 
			
		
		
		
	Merge pull request #89660 from pjferrell/kubectl-jsonpath-nonprimitive-types
client-go/util/jsonpath: resolve #16707 by outputting json for non-primitive types
This commit is contained in:
		| @@ -24,7 +24,7 @@ import ( | ||||
| 	"regexp" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/api/core/v1" | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| @@ -420,7 +420,7 @@ abcdef.1234567890123456   <forever>   <never>   signing,authentication   valid b | ||||
| 			usages:       []string{"signing", "authentication"}, | ||||
| 			extraGroups:  []string{"system:bootstrappers:kubeadm:default-node-token"}, | ||||
| 			outputFormat: "jsonpath={.token} {.groups}", | ||||
| 			expected:     "abcdef.1234567890123456 [system:bootstrappers:kubeadm:default-node-token]", | ||||
| 			expected:     "abcdef.1234567890123456 [\"system:bootstrappers:kubeadm:default-node-token\"]", | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range testCases { | ||||
|   | ||||
| @@ -245,7 +245,7 @@ func TestTableGet(t *testing.T) { | ||||
| 				if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected { | ||||
| 					t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got) | ||||
| 				} | ||||
| 				if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected { | ||||
| 				if got, expected := tbl.Rows[0].Cells[5], "[1,2,3]"; got != expected { | ||||
| 					t.Errorf("expected cell[5] to equal %q, got %q", expected, got) | ||||
| 				} | ||||
| 				// Validate extra column for v1 | ||||
| @@ -343,7 +343,7 @@ func TestTableGet(t *testing.T) { | ||||
| 				if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected { | ||||
| 					t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got) | ||||
| 				} | ||||
| 				if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected { | ||||
| 				if got, expected := tbl.Rows[0].Cells[5], "[1,2,3]"; got != expected { | ||||
| 					t.Errorf("expected cell[5] to equal %q, got %q", expected, got) | ||||
| 				} | ||||
| 				// Validate extra column for v1 | ||||
|   | ||||
| @@ -31,8 +31,9 @@ import ( | ||||
| // this allows a user to specify a template format value | ||||
| // as --output=jsonpath= | ||||
| var jsonFormats = map[string]bool{ | ||||
| 	"jsonpath":      true, | ||||
| 	"jsonpath-file": true, | ||||
| 	"jsonpath":         true, | ||||
| 	"jsonpath-file":    true, | ||||
| 	"jsonpath-as-json": true, | ||||
| } | ||||
|  | ||||
| // JSONPathPrintFlags provides default flags necessary for template printing. | ||||
| @@ -105,6 +106,11 @@ func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.Resource | ||||
| 	} | ||||
|  | ||||
| 	p.AllowMissingKeys(allowMissingKeys) | ||||
|  | ||||
| 	if templateFormat == "jsonpath-as-json" { | ||||
| 		p.EnableJSONOutput(true) | ||||
| 	} | ||||
|  | ||||
| 	return p, nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -25,7 +25,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/api/core/v1" | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| ) | ||||
|  | ||||
| @@ -68,6 +68,22 @@ func TestPrinterSupportsExpectedJSONPathFormats(t *testing.T) { | ||||
| 			templateArg:    "{ .metadata.name }", | ||||
| 			expectedOutput: "foo", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "valid jsonpath-as-json output format also containing argument succeeds", | ||||
| 			outputFormat:   "jsonpath-as-json={ .metadata.name }", | ||||
| 			expectedOutput: "foo", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "valid jsonpath-as-json output format and no --template argument results in an error", | ||||
| 			outputFormat:  "jsonpath-as-json", | ||||
| 			expectedError: "template format specified but no template given", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "valid jsonpath-as-json output format and --template argument succeeds", | ||||
| 			outputFormat:   "jsonpath-as-json", | ||||
| 			templateArg:    "{ .metadata.name }", | ||||
| 			expectedOutput: "foo", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "jsonpath template file should match, and successfully return correct value", | ||||
| 			outputFormat:   "jsonpath-file", | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import ( | ||||
| 	"bytes" | ||||
| 	"testing" | ||||
|  | ||||
| 	"k8s.io/api/core/v1" | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| @@ -58,4 +58,18 @@ func TestPrinters(t *testing.T) { | ||||
| 			t.Errorf("JSONPathPrinter error object '%v'; error: '%v'", oName, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// rerun tests with JSONOutput enabled | ||||
| 	jsonpathPrinter.EnableJSONOutput(true) | ||||
|  | ||||
| 	for oName, obj := range objects { | ||||
| 		b := &bytes.Buffer{} | ||||
| 		if err := jsonpathPrinter.PrintObj(obj, b); err != nil { | ||||
| 			if expectedErrors.Has(oName) { | ||||
| 				// expected error | ||||
| 				continue | ||||
| 			} | ||||
| 			t.Errorf("JSONPathPrinter jsonOutput error object '%v'; error: '%v'", oName, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ package jsonpath | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"reflect" | ||||
| @@ -36,6 +37,7 @@ type JSONPath struct { | ||||
| 	lastEndNode *Node | ||||
|  | ||||
| 	allowMissingKeys bool | ||||
| 	outputJSON       bool | ||||
| } | ||||
|  | ||||
| // New creates a new JSONPath with the given name. | ||||
| @@ -125,10 +127,49 @@ func (j *JSONPath) FindResults(data interface{}) ([][]reflect.Value, error) { | ||||
| 	return fullResult, nil | ||||
| } | ||||
|  | ||||
| // EnableJSONOutput changes the PrintResults behavior to return a JSON array of results | ||||
| func (j *JSONPath) EnableJSONOutput(v bool) { | ||||
| 	j.outputJSON = v | ||||
| } | ||||
|  | ||||
| // PrintResults writes the results into writer | ||||
| func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error { | ||||
| 	if j.outputJSON { | ||||
| 		// convert the []reflect.Value to something that json | ||||
| 		// will be able to marshal | ||||
| 		r := make([]interface{}, 0, len(results)) | ||||
| 		for i := range results { | ||||
| 			r = append(r, results[i].Interface()) | ||||
| 		} | ||||
| 		results = []reflect.Value{reflect.ValueOf(r)} | ||||
| 	} | ||||
| 	for i, r := range results { | ||||
| 		text, err := j.evalToText(r) | ||||
| 		var text []byte | ||||
| 		var err error | ||||
| 		outputJSON := true | ||||
| 		kind := r.Kind() | ||||
| 		if kind == reflect.Interface { | ||||
| 			kind = r.Elem().Kind() | ||||
| 		} | ||||
| 		switch kind { | ||||
| 		case reflect.Map: | ||||
| 		case reflect.Array: | ||||
| 		case reflect.Slice: | ||||
| 		case reflect.Struct: | ||||
| 		default: | ||||
| 			outputJSON = false | ||||
| 		} | ||||
| 		switch { | ||||
| 		case outputJSON || j.outputJSON: | ||||
| 			if j.outputJSON { | ||||
| 				text, err = json.MarshalIndent(r.Interface(), "", "    ") | ||||
| 				text = append(text, '\n') | ||||
| 			} else { | ||||
| 				text, err = json.Marshal(r.Interface()) | ||||
| 			} | ||||
| 		default: | ||||
| 			text, err = j.evalToText(r) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @@ -139,7 +180,9 @@ func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // walk visits tree rooted at the given node in DFS order | ||||
|   | ||||
| @@ -49,7 +49,7 @@ func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) { | ||||
| 		err = j.Execute(buf, test.input) | ||||
| 		if test.expectError { | ||||
| 			if err == nil { | ||||
| 				t.Errorf("in %s, expected execute error", test.name) | ||||
| 				t.Errorf(`in %s, expected execute error, got %q`, test.name, buf) | ||||
| 			} | ||||
| 			continue | ||||
| 		} else if err != nil { | ||||
| @@ -108,6 +108,94 @@ func testFailJSONPath(tests []jsonpathTest, t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestTypesInput(t *testing.T) { | ||||
| 	types := map[string]interface{}{ | ||||
| 		"bools":      []bool{true, false, true, false}, | ||||
| 		"integers":   []int{1, 2, 3, 4}, | ||||
| 		"floats":     []float64{1.0, 2.2, 3.3, 4.0}, | ||||
| 		"strings":    []string{"one", "two", "three", "four"}, | ||||
| 		"interfaces": []interface{}{true, "one", 1, 1.1}, | ||||
| 		"maps": []map[string]interface{}{ | ||||
| 			{"name": "one", "value": 1}, | ||||
| 			{"name": "two", "value": 2.02}, | ||||
| 			{"name": "three", "value": 3.03}, | ||||
| 			{"name": "four", "value": 4.04}, | ||||
| 		}, | ||||
| 		"structs": []struct { | ||||
| 			Name  string      `json:"name"` | ||||
| 			Value interface{} `json:"value"` | ||||
| 			Type  string      `json:"type"` | ||||
| 		}{ | ||||
| 			{Name: "one", Value: 1, Type: "integer"}, | ||||
| 			{Name: "two", Value: 2.002, Type: "float"}, | ||||
| 			{Name: "three", Value: 3, Type: "integer"}, | ||||
| 			{Name: "four", Value: 4.004, Type: "float"}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	sliceTests := []jsonpathTest{ | ||||
| 		// boolean slice tests | ||||
| 		{"boolSlice", `{ .bools }`, types, `[true,false,true,false]`, false}, | ||||
| 		{"boolSliceIndex", `{ .bools[0] }`, types, `true`, false}, | ||||
| 		{"boolSliceIndex", `{ .bools[-1] }`, types, `false`, false}, | ||||
| 		{"boolSubSlice", `{ .bools[0:2] }`, types, `true false`, false}, | ||||
| 		{"boolSubSliceFirst2", `{ .bools[:2] }`, types, `true false`, false}, | ||||
| 		{"boolSubSliceStep2", `{ .bools[:4:2] }`, types, `true true`, false}, | ||||
| 		// integer slice tests | ||||
| 		{"integerSlice", `{ .integers }`, types, `[1,2,3,4]`, false}, | ||||
| 		{"integerSliceIndex", `{ .integers[0] }`, types, `1`, false}, | ||||
| 		{"integerSliceIndexReverse", `{ .integers[-2] }`, types, `3`, false}, | ||||
| 		{"integerSubSliceFirst2", `{ .integers[0:2] }`, types, `1 2`, false}, | ||||
| 		{"integerSubSliceFirst2Alt", `{ .integers[:2] }`, types, `1 2`, false}, | ||||
| 		{"integerSubSliceStep2", `{ .integers[:4:2] }`, types, `1 3`, false}, | ||||
| 		// float slice tests | ||||
| 		{"floatSlice", `{ .floats }`, types, `[1,2.2,3.3,4]`, false}, | ||||
| 		{"floatSliceIndex", `{ .floats[0] }`, types, `1`, false}, | ||||
| 		{"floatSliceIndexReverse", `{ .floats[-2] }`, types, `3.3`, false}, | ||||
| 		{"floatSubSliceFirst2", `{ .floats[0:2] }`, types, `1 2.2`, false}, | ||||
| 		{"floatSubSliceFirst2Alt", `{ .floats[:2] }`, types, `1 2.2`, false}, | ||||
| 		{"floatSubSliceStep2", `{ .floats[:4:2] }`, types, `1 3.3`, false}, | ||||
| 		// strings slice tests | ||||
| 		{"stringSlice", `{ .strings }`, types, `["one","two","three","four"]`, false}, | ||||
| 		{"stringSliceIndex", `{ .strings[0] }`, types, `one`, false}, | ||||
| 		{"stringSliceIndexReverse", `{ .strings[-2] }`, types, `three`, false}, | ||||
| 		{"stringSubSliceFirst2", `{ .strings[0:2] }`, types, `one two`, false}, | ||||
| 		{"stringSubSliceFirst2Alt", `{ .strings[:2] }`, types, `one two`, false}, | ||||
| 		{"stringSubSliceStep2", `{ .strings[:4:2] }`, types, `one three`, false}, | ||||
| 		// interfaces slice tests | ||||
| 		{"interfaceSlice", `{ .interfaces }`, types, `[true,"one",1,1.1]`, false}, | ||||
| 		{"interfaceSliceIndex", `{ .interfaces[0] }`, types, `true`, false}, | ||||
| 		{"interfaceSliceIndexReverse", `{ .interfaces[-2] }`, types, `1`, false}, | ||||
| 		{"interfaceSubSliceFirst2", `{ .interfaces[0:2] }`, types, `true one`, false}, | ||||
| 		{"interfaceSubSliceFirst2Alt", `{ .interfaces[:2] }`, types, `true one`, false}, | ||||
| 		{"interfaceSubSliceStep2", `{ .interfaces[:4:2] }`, types, `true 1`, false}, | ||||
| 		// maps slice tests | ||||
| 		{"mapSlice", `{ .maps }`, types, | ||||
| 			`[{"name":"one","value":1},{"name":"two","value":2.02},{"name":"three","value":3.03},{"name":"four","value":4.04}]`, false}, | ||||
| 		{"mapSliceIndex", `{ .maps[0] }`, types, `{"name":"one","value":1}`, false}, | ||||
| 		{"mapSliceIndexReverse", `{ .maps[-2] }`, types, `{"name":"three","value":3.03}`, false}, | ||||
| 		{"mapSubSliceFirst2", `{ .maps[0:2] }`, types, `{"name":"one","value":1} {"name":"two","value":2.02}`, false}, | ||||
| 		{"mapSubSliceFirst2Alt", `{ .maps[:2] }`, types, `{"name":"one","value":1} {"name":"two","value":2.02}`, false}, | ||||
| 		{"mapSubSliceStepOdd", `{ .maps[::2] }`, types, `{"name":"one","value":1} {"name":"three","value":3.03}`, false}, | ||||
| 		{"mapSubSliceStepEven", `{ .maps[1::2] }`, types, `{"name":"two","value":2.02} {"name":"four","value":4.04}`, false}, | ||||
| 		// structs slice tests | ||||
| 		{"structSlice", `{ .structs }`, types, | ||||
| 			`[{"name":"one","value":1,"type":"integer"},{"name":"two","value":2.002,"type":"float"},{"name":"three","value":3,"type":"integer"},{"name":"four","value":4.004,"type":"float"}]`, false}, | ||||
| 		{"structSliceIndex", `{ .structs[0] }`, types, `{"name":"one","value":1,"type":"integer"}`, false}, | ||||
| 		{"structSliceIndexReverse", `{ .structs[-2] }`, types, `{"name":"three","value":3,"type":"integer"}`, false}, | ||||
| 		{"structSubSliceFirst2", `{ .structs[0:2] }`, types, | ||||
| 			`{"name":"one","value":1,"type":"integer"} {"name":"two","value":2.002,"type":"float"}`, false}, | ||||
| 		{"structSubSliceFirst2Alt", `{ .structs[:2] }`, types, | ||||
| 			`{"name":"one","value":1,"type":"integer"} {"name":"two","value":2.002,"type":"float"}`, false}, | ||||
| 		{"structSubSliceStepOdd", `{ .structs[::2] }`, types, | ||||
| 			`{"name":"one","value":1,"type":"integer"} {"name":"three","value":3,"type":"integer"}`, false}, | ||||
| 		{"structSubSliceStepEven", `{ .structs[1::2] }`, types, | ||||
| 			`{"name":"two","value":2.002,"type":"float"} {"name":"four","value":4.004,"type":"float"}`, false}, | ||||
| 	} | ||||
|  | ||||
| 	testJSONPath(sliceTests, false, t) | ||||
| } | ||||
|  | ||||
| type book struct { | ||||
| 	Category string | ||||
| 	Author   string | ||||
| @@ -161,7 +249,7 @@ func TestStructInput(t *testing.T) { | ||||
|  | ||||
| 	storeTests := []jsonpathTest{ | ||||
| 		{"plain", "hello jsonpath", nil, "hello jsonpath", false}, | ||||
| 		{"recursive", "{..}", []int{1, 2, 3}, "[1 2 3]", false}, | ||||
| 		{"recursive", "{..}", []int{1, 2, 3}, "[1,2,3]", false}, | ||||
| 		{"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false}, | ||||
| 		{"quote", `{"{"}`, nil, "{", false}, | ||||
| 		{"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false}, | ||||
| @@ -173,14 +261,19 @@ func TestStructInput(t *testing.T) { | ||||
| 		{"dict-", "{.Labels.k8s-app}", storeData, "20", false}, | ||||
| 		{"nest", "{.Bicycle[*].Color}", storeData, "red green", false}, | ||||
| 		{"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false}, | ||||
| 		{"allfileds", "{.Bicycle.*}", storeData, "{red 19.95 true} {green 20.01 false}", false}, | ||||
| 		{"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false}, | ||||
| 		{"allfields", `{range .Bicycle[*]}{ "{" }{ @.* }{ "} " }{end}`, storeData, "{red 19.95 true} {green 20.01 false} ", false}, | ||||
| 		{"recurfields", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false}, | ||||
| 		{"allstructsSlice", "{.Bicycle}", storeData, | ||||
| 			`[{"Color":"red","Price":19.95,"IsNew":true},{"Color":"green","Price":20.01,"IsNew":false}]`, false}, | ||||
| 		{"allstructs", `{range .Bicycle[*]}{ @ }{ " " }{end}`, storeData, | ||||
| 			`{"Color":"red","Price":19.95,"IsNew":true} {"Color":"green","Price":20.01,"IsNew":false} `, false}, | ||||
| 		{"lastarray", "{.Book[-1:]}", storeData, | ||||
| 			"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false}, | ||||
| 			`{"Category":"fiction","Author":"Herman Melville","Title":"Moby Dick","Price":8.99}`, false}, | ||||
| 		{"recurarray", "{..Book[2]}", storeData, | ||||
| 			"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false}, | ||||
| 		{"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, "{red 19.95 true}", false}, | ||||
| 			`{"Category":"fiction","Author":"Herman Melville","Title":"Moby Dick","Price":8.99}`, false}, | ||||
| 		{"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, `{"Color":"red","Price":19.95,"IsNew":true}`, false}, | ||||
| 	} | ||||
|  | ||||
| 	testJSONPath(storeTests, false, t) | ||||
|  | ||||
| 	missingKeyTests := []jsonpathTest{ | ||||
| @@ -282,9 +375,9 @@ func TestKubernetes(t *testing.T) { | ||||
| 			"127.0.0.1, 127.0.0.2, 127.0.0.3, ", false}, | ||||
| 		{"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false}, | ||||
| 		{"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData, | ||||
| 			"127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]", false}, | ||||
| 			`127.0.0.1 127.0.0.2 {"cpu":"4"} {"cpu":"8"}`, false}, | ||||
| 		{"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData, | ||||
| 			"[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] ", false}, | ||||
| 			`[127.0.0.1, {"cpu":"4"}] [127.0.0.2, {"cpu":"8"}] `, false}, | ||||
| 		{"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false}, | ||||
| 		{"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false}, | ||||
| 		{"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false}, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot