Skip to content

Commit 0c3c2cd

Browse files
authored
Merge pull request kubernetes#89660 from pjferrell/kubectl-jsonpath-nonprimitive-types
client-go/util/jsonpath: resolve kubernetes#16707 by outputting json for non-primitive types
2 parents 961a5ed + f092c38 commit 0c3c2cd

File tree

7 files changed

+190
-18
lines changed

7 files changed

+190
-18
lines changed

cmd/kubeadm/app/cmd/token_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"regexp"
2525
"testing"
2626

27-
"k8s.io/api/core/v1"
27+
v1 "k8s.io/api/core/v1"
2828
apierrors "k8s.io/apimachinery/pkg/api/errors"
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3030
"k8s.io/apimachinery/pkg/runtime"
@@ -420,7 +420,7 @@ abcdef.1234567890123456 <forever> <never> signing,authentication valid b
420420
usages: []string{"signing", "authentication"},
421421
extraGroups: []string{"system:bootstrappers:kubeadm:default-node-token"},
422422
outputFormat: "jsonpath={.token} {.groups}",
423-
expected: "abcdef.1234567890123456 [system:bootstrappers:kubeadm:default-node-token]",
423+
expected: "abcdef.1234567890123456 [\"system:bootstrappers:kubeadm:default-node-token\"]",
424424
},
425425
}
426426
for _, tc := range testCases {

staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func TestTableGet(t *testing.T) {
245245
if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected {
246246
t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got)
247247
}
248-
if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected {
248+
if got, expected := tbl.Rows[0].Cells[5], "[1,2,3]"; got != expected {
249249
t.Errorf("expected cell[5] to equal %q, got %q", expected, got)
250250
}
251251
// Validate extra column for v1
@@ -343,7 +343,7 @@ func TestTableGet(t *testing.T) {
343343
if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected {
344344
t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got)
345345
}
346-
if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected {
346+
if got, expected := tbl.Rows[0].Cells[5], "[1,2,3]"; got != expected {
347347
t.Errorf("expected cell[5] to equal %q, got %q", expected, got)
348348
}
349349
// Validate extra column for v1

staging/src/k8s.io/cli-runtime/pkg/genericclioptions/jsonpath_flags.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ import (
3131
// this allows a user to specify a template format value
3232
// as --output=jsonpath=
3333
var jsonFormats = map[string]bool{
34-
"jsonpath": true,
35-
"jsonpath-file": true,
34+
"jsonpath": true,
35+
"jsonpath-file": true,
36+
"jsonpath-as-json": true,
3637
}
3738

3839
// JSONPathPrintFlags provides default flags necessary for template printing.
@@ -105,6 +106,11 @@ func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.Resource
105106
}
106107

107108
p.AllowMissingKeys(allowMissingKeys)
109+
110+
if templateFormat == "jsonpath-as-json" {
111+
p.EnableJSONOutput(true)
112+
}
113+
108114
return p, nil
109115
}
110116

staging/src/k8s.io/cli-runtime/pkg/genericclioptions/jsonpath_flags_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"strings"
2626
"testing"
2727

28-
"k8s.io/api/core/v1"
28+
v1 "k8s.io/api/core/v1"
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3030
)
3131

@@ -68,6 +68,22 @@ func TestPrinterSupportsExpectedJSONPathFormats(t *testing.T) {
6868
templateArg: "{ .metadata.name }",
6969
expectedOutput: "foo",
7070
},
71+
{
72+
name: "valid jsonpath-as-json output format also containing argument succeeds",
73+
outputFormat: "jsonpath-as-json={ .metadata.name }",
74+
expectedOutput: "foo",
75+
},
76+
{
77+
name: "valid jsonpath-as-json output format and no --template argument results in an error",
78+
outputFormat: "jsonpath-as-json",
79+
expectedError: "template format specified but no template given",
80+
},
81+
{
82+
name: "valid jsonpath-as-json output format and --template argument succeeds",
83+
outputFormat: "jsonpath-as-json",
84+
templateArg: "{ .metadata.name }",
85+
expectedOutput: "foo",
86+
},
7187
{
7288
name: "jsonpath template file should match, and successfully return correct value",
7389
outputFormat: "jsonpath-file",

staging/src/k8s.io/cli-runtime/pkg/printers/jsonpath_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"bytes"
2121
"testing"
2222

23-
"k8s.io/api/core/v1"
23+
v1 "k8s.io/api/core/v1"
2424
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2525
"k8s.io/apimachinery/pkg/runtime"
2626
"k8s.io/apimachinery/pkg/util/sets"
@@ -58,4 +58,18 @@ func TestPrinters(t *testing.T) {
5858
t.Errorf("JSONPathPrinter error object '%v'; error: '%v'", oName, err)
5959
}
6060
}
61+
62+
// rerun tests with JSONOutput enabled
63+
jsonpathPrinter.EnableJSONOutput(true)
64+
65+
for oName, obj := range objects {
66+
b := &bytes.Buffer{}
67+
if err := jsonpathPrinter.PrintObj(obj, b); err != nil {
68+
if expectedErrors.Has(oName) {
69+
// expected error
70+
continue
71+
}
72+
t.Errorf("JSONPathPrinter jsonOutput error object '%v'; error: '%v'", oName, err)
73+
}
74+
}
6175
}

staging/src/k8s.io/client-go/util/jsonpath/jsonpath.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package jsonpath
1818

1919
import (
2020
"bytes"
21+
"encoding/json"
2122
"fmt"
2223
"io"
2324
"reflect"
@@ -36,6 +37,7 @@ type JSONPath struct {
3637
lastEndNode *Node
3738

3839
allowMissingKeys bool
40+
outputJSON bool
3941
}
4042

4143
// New creates a new JSONPath with the given name.
@@ -125,10 +127,49 @@ func (j *JSONPath) FindResults(data interface{}) ([][]reflect.Value, error) {
125127
return fullResult, nil
126128
}
127129

130+
// EnableJSONOutput changes the PrintResults behavior to return a JSON array of results
131+
func (j *JSONPath) EnableJSONOutput(v bool) {
132+
j.outputJSON = v
133+
}
134+
128135
// PrintResults writes the results into writer
129136
func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error {
137+
if j.outputJSON {
138+
// convert the []reflect.Value to something that json
139+
// will be able to marshal
140+
r := make([]interface{}, 0, len(results))
141+
for i := range results {
142+
r = append(r, results[i].Interface())
143+
}
144+
results = []reflect.Value{reflect.ValueOf(r)}
145+
}
130146
for i, r := range results {
131-
text, err := j.evalToText(r)
147+
var text []byte
148+
var err error
149+
outputJSON := true
150+
kind := r.Kind()
151+
if kind == reflect.Interface {
152+
kind = r.Elem().Kind()
153+
}
154+
switch kind {
155+
case reflect.Map:
156+
case reflect.Array:
157+
case reflect.Slice:
158+
case reflect.Struct:
159+
default:
160+
outputJSON = false
161+
}
162+
switch {
163+
case outputJSON || j.outputJSON:
164+
if j.outputJSON {
165+
text, err = json.MarshalIndent(r.Interface(), "", " ")
166+
text = append(text, '\n')
167+
} else {
168+
text, err = json.Marshal(r.Interface())
169+
}
170+
default:
171+
text, err = j.evalToText(r)
172+
}
132173
if err != nil {
133174
return err
134175
}
@@ -139,7 +180,9 @@ func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error {
139180
return err
140181
}
141182
}
183+
142184
return nil
185+
143186
}
144187

145188
// walk visits tree rooted at the given node in DFS order

staging/src/k8s.io/client-go/util/jsonpath/jsonpath_test.go

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func testJSONPath(tests []jsonpathTest, allowMissingKeys bool, t *testing.T) {
4949
err = j.Execute(buf, test.input)
5050
if test.expectError {
5151
if err == nil {
52-
t.Errorf("in %s, expected execute error", test.name)
52+
t.Errorf(`in %s, expected execute error, got %q`, test.name, buf)
5353
}
5454
continue
5555
} else if err != nil {
@@ -108,6 +108,94 @@ func testFailJSONPath(tests []jsonpathTest, t *testing.T) {
108108
}
109109
}
110110

111+
func TestTypesInput(t *testing.T) {
112+
types := map[string]interface{}{
113+
"bools": []bool{true, false, true, false},
114+
"integers": []int{1, 2, 3, 4},
115+
"floats": []float64{1.0, 2.2, 3.3, 4.0},
116+
"strings": []string{"one", "two", "three", "four"},
117+
"interfaces": []interface{}{true, "one", 1, 1.1},
118+
"maps": []map[string]interface{}{
119+
{"name": "one", "value": 1},
120+
{"name": "two", "value": 2.02},
121+
{"name": "three", "value": 3.03},
122+
{"name": "four", "value": 4.04},
123+
},
124+
"structs": []struct {
125+
Name string `json:"name"`
126+
Value interface{} `json:"value"`
127+
Type string `json:"type"`
128+
}{
129+
{Name: "one", Value: 1, Type: "integer"},
130+
{Name: "two", Value: 2.002, Type: "float"},
131+
{Name: "three", Value: 3, Type: "integer"},
132+
{Name: "four", Value: 4.004, Type: "float"},
133+
},
134+
}
135+
136+
sliceTests := []jsonpathTest{
137+
// boolean slice tests
138+
{"boolSlice", `{ .bools }`, types, `[true,false,true,false]`, false},
139+
{"boolSliceIndex", `{ .bools[0] }`, types, `true`, false},
140+
{"boolSliceIndex", `{ .bools[-1] }`, types, `false`, false},
141+
{"boolSubSlice", `{ .bools[0:2] }`, types, `true false`, false},
142+
{"boolSubSliceFirst2", `{ .bools[:2] }`, types, `true false`, false},
143+
{"boolSubSliceStep2", `{ .bools[:4:2] }`, types, `true true`, false},
144+
// integer slice tests
145+
{"integerSlice", `{ .integers }`, types, `[1,2,3,4]`, false},
146+
{"integerSliceIndex", `{ .integers[0] }`, types, `1`, false},
147+
{"integerSliceIndexReverse", `{ .integers[-2] }`, types, `3`, false},
148+
{"integerSubSliceFirst2", `{ .integers[0:2] }`, types, `1 2`, false},
149+
{"integerSubSliceFirst2Alt", `{ .integers[:2] }`, types, `1 2`, false},
150+
{"integerSubSliceStep2", `{ .integers[:4:2] }`, types, `1 3`, false},
151+
// float slice tests
152+
{"floatSlice", `{ .floats }`, types, `[1,2.2,3.3,4]`, false},
153+
{"floatSliceIndex", `{ .floats[0] }`, types, `1`, false},
154+
{"floatSliceIndexReverse", `{ .floats[-2] }`, types, `3.3`, false},
155+
{"floatSubSliceFirst2", `{ .floats[0:2] }`, types, `1 2.2`, false},
156+
{"floatSubSliceFirst2Alt", `{ .floats[:2] }`, types, `1 2.2`, false},
157+
{"floatSubSliceStep2", `{ .floats[:4:2] }`, types, `1 3.3`, false},
158+
// strings slice tests
159+
{"stringSlice", `{ .strings }`, types, `["one","two","three","four"]`, false},
160+
{"stringSliceIndex", `{ .strings[0] }`, types, `one`, false},
161+
{"stringSliceIndexReverse", `{ .strings[-2] }`, types, `three`, false},
162+
{"stringSubSliceFirst2", `{ .strings[0:2] }`, types, `one two`, false},
163+
{"stringSubSliceFirst2Alt", `{ .strings[:2] }`, types, `one two`, false},
164+
{"stringSubSliceStep2", `{ .strings[:4:2] }`, types, `one three`, false},
165+
// interfaces slice tests
166+
{"interfaceSlice", `{ .interfaces }`, types, `[true,"one",1,1.1]`, false},
167+
{"interfaceSliceIndex", `{ .interfaces[0] }`, types, `true`, false},
168+
{"interfaceSliceIndexReverse", `{ .interfaces[-2] }`, types, `1`, false},
169+
{"interfaceSubSliceFirst2", `{ .interfaces[0:2] }`, types, `true one`, false},
170+
{"interfaceSubSliceFirst2Alt", `{ .interfaces[:2] }`, types, `true one`, false},
171+
{"interfaceSubSliceStep2", `{ .interfaces[:4:2] }`, types, `true 1`, false},
172+
// maps slice tests
173+
{"mapSlice", `{ .maps }`, types,
174+
`[{"name":"one","value":1},{"name":"two","value":2.02},{"name":"three","value":3.03},{"name":"four","value":4.04}]`, false},
175+
{"mapSliceIndex", `{ .maps[0] }`, types, `{"name":"one","value":1}`, false},
176+
{"mapSliceIndexReverse", `{ .maps[-2] }`, types, `{"name":"three","value":3.03}`, false},
177+
{"mapSubSliceFirst2", `{ .maps[0:2] }`, types, `{"name":"one","value":1} {"name":"two","value":2.02}`, false},
178+
{"mapSubSliceFirst2Alt", `{ .maps[:2] }`, types, `{"name":"one","value":1} {"name":"two","value":2.02}`, false},
179+
{"mapSubSliceStepOdd", `{ .maps[::2] }`, types, `{"name":"one","value":1} {"name":"three","value":3.03}`, false},
180+
{"mapSubSliceStepEven", `{ .maps[1::2] }`, types, `{"name":"two","value":2.02} {"name":"four","value":4.04}`, false},
181+
// structs slice tests
182+
{"structSlice", `{ .structs }`, types,
183+
`[{"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},
184+
{"structSliceIndex", `{ .structs[0] }`, types, `{"name":"one","value":1,"type":"integer"}`, false},
185+
{"structSliceIndexReverse", `{ .structs[-2] }`, types, `{"name":"three","value":3,"type":"integer"}`, false},
186+
{"structSubSliceFirst2", `{ .structs[0:2] }`, types,
187+
`{"name":"one","value":1,"type":"integer"} {"name":"two","value":2.002,"type":"float"}`, false},
188+
{"structSubSliceFirst2Alt", `{ .structs[:2] }`, types,
189+
`{"name":"one","value":1,"type":"integer"} {"name":"two","value":2.002,"type":"float"}`, false},
190+
{"structSubSliceStepOdd", `{ .structs[::2] }`, types,
191+
`{"name":"one","value":1,"type":"integer"} {"name":"three","value":3,"type":"integer"}`, false},
192+
{"structSubSliceStepEven", `{ .structs[1::2] }`, types,
193+
`{"name":"two","value":2.002,"type":"float"} {"name":"four","value":4.004,"type":"float"}`, false},
194+
}
195+
196+
testJSONPath(sliceTests, false, t)
197+
}
198+
111199
type book struct {
112200
Category string
113201
Author string
@@ -161,7 +249,7 @@ func TestStructInput(t *testing.T) {
161249

162250
storeTests := []jsonpathTest{
163251
{"plain", "hello jsonpath", nil, "hello jsonpath", false},
164-
{"recursive", "{..}", []int{1, 2, 3}, "[1 2 3]", false},
252+
{"recursive", "{..}", []int{1, 2, 3}, "[1,2,3]", false},
165253
{"filter", "{[?(@<5)]}", []int{2, 6, 3, 7}, "2 3", false},
166254
{"quote", `{"{"}`, nil, "{", false},
167255
{"union", "{[1,3,4]}", []int{0, 1, 2, 3, 4}, "1 3 4", false},
@@ -173,14 +261,19 @@ func TestStructInput(t *testing.T) {
173261
{"dict-", "{.Labels.k8s-app}", storeData, "20", false},
174262
{"nest", "{.Bicycle[*].Color}", storeData, "red green", false},
175263
{"allarray", "{.Book[*].Author}", storeData, "Nigel Rees Evelyn Waugh Herman Melville", false},
176-
{"allfileds", "{.Bicycle.*}", storeData, "{red 19.95 true} {green 20.01 false}", false},
177-
{"recurfileds", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false},
264+
{"allfields", `{range .Bicycle[*]}{ "{" }{ @.* }{ "} " }{end}`, storeData, "{red 19.95 true} {green 20.01 false} ", false},
265+
{"recurfields", "{..Price}", storeData, "8.95 12.99 8.99 19.95 20.01", false},
266+
{"allstructsSlice", "{.Bicycle}", storeData,
267+
`[{"Color":"red","Price":19.95,"IsNew":true},{"Color":"green","Price":20.01,"IsNew":false}]`, false},
268+
{"allstructs", `{range .Bicycle[*]}{ @ }{ " " }{end}`, storeData,
269+
`{"Color":"red","Price":19.95,"IsNew":true} {"Color":"green","Price":20.01,"IsNew":false} `, false},
178270
{"lastarray", "{.Book[-1:]}", storeData,
179-
"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false},
271+
`{"Category":"fiction","Author":"Herman Melville","Title":"Moby Dick","Price":8.99}`, false},
180272
{"recurarray", "{..Book[2]}", storeData,
181-
"{Category: fiction, Author: Herman Melville, Title: Moby Dick, Price: 8.99}", false},
182-
{"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, "{red 19.95 true}", false},
273+
`{"Category":"fiction","Author":"Herman Melville","Title":"Moby Dick","Price":8.99}`, false},
274+
{"bool", "{.Bicycle[?(@.IsNew==true)]}", storeData, `{"Color":"red","Price":19.95,"IsNew":true}`, false},
183275
}
276+
184277
testJSONPath(storeTests, false, t)
185278

186279
missingKeyTests := []jsonpathTest{
@@ -282,9 +375,9 @@ func TestKubernetes(t *testing.T) {
282375
"127.0.0.1, 127.0.0.2, 127.0.0.3, ", false},
283376
{"item name", `{.items[*].metadata.name}`, nodesData, "127.0.0.1 127.0.0.2", false},
284377
{"union nodes capacity", `{.items[*]['metadata.name', 'status.capacity']}`, nodesData,
285-
"127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]", false},
378+
`127.0.0.1 127.0.0.2 {"cpu":"4"} {"cpu":"8"}`, false},
286379
{"range nodes capacity", `{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}`, nodesData,
287-
"[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]] ", false},
380+
`[127.0.0.1, {"cpu":"4"}] [127.0.0.2, {"cpu":"8"}] `, false},
288381
{"user password", `{.users[?(@.name=="e2e")].user.password}`, &nodesData, "secret", false},
289382
{"hostname", `{.items[0].metadata.labels.kubernetes\.io/hostname}`, &nodesData, "127.0.0.1", false},
290383
{"hostname filter", `{.items[?(@.metadata.labels.kubernetes\.io/hostname=="127.0.0.1")].kind}`, &nodesData, "None", false},

0 commit comments

Comments
 (0)