1
16
17 package tableconvertor
18
19 import (
20 "context"
21 "fmt"
22 "reflect"
23 "testing"
24 "time"
25
26 "github.com/google/go-cmp/cmp"
27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
29 metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/client-go/util/jsonpath"
32 )
33
34 func Test_cellForJSONValue(t *testing.T) {
35 tests := []struct {
36 headerType string
37 value interface{}
38 want interface{}
39 }{
40 {"integer", int64(42), int64(42)},
41 {"integer", float64(3.14), int64(3)},
42 {"integer", true, nil},
43 {"integer", "foo", nil},
44
45 {"number", int64(42), float64(42)},
46 {"number", float64(3.14), float64(3.14)},
47 {"number", true, nil},
48 {"number", "foo", nil},
49
50 {"boolean", int64(42), nil},
51 {"boolean", float64(3.14), nil},
52 {"boolean", true, true},
53 {"boolean", "foo", nil},
54
55 {"string", int64(42), nil},
56 {"string", float64(3.14), nil},
57 {"string", true, nil},
58 {"string", "foo", "foo"},
59
60 {"date", int64(42), nil},
61 {"date", float64(3.14), nil},
62 {"date", true, nil},
63 {"date", time.Now().Add(-time.Hour*12 - 30*time.Minute).UTC().Format(time.RFC3339), "12h"},
64 {"date", time.Now().Add(+time.Hour*12 + 30*time.Minute).UTC().Format(time.RFC3339), "<invalid>"},
65 {"date", "", "<unknown>"},
66
67 {"unknown", "foo", nil},
68 }
69 for _, tt := range tests {
70 t.Run(fmt.Sprintf("%#v of type %s", tt.value, tt.headerType), func(t *testing.T) {
71 if got := cellForJSONValue(tt.headerType, tt.value); !reflect.DeepEqual(got, tt.want) {
72 t.Errorf("cellForJSONValue() = %#v, want %#v", got, tt.want)
73 }
74 })
75 }
76 }
77
78 func Test_convertor_ConvertToTable(t *testing.T) {
79 type fields struct {
80 headers []metav1.TableColumnDefinition
81 additionalColumns []columnPrinter
82 }
83 type args struct {
84 ctx context.Context
85 obj runtime.Object
86 tableOptions runtime.Object
87 }
88 tests := []struct {
89 name string
90 fields fields
91 args args
92 want *metav1.Table
93 wantErr bool
94 }{
95 {
96 name: "Return table for object",
97 fields: fields{
98 headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
99 },
100 args: args{
101 obj: &metav1beta1.PartialObjectMetadata{
102 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
103 },
104 tableOptions: nil,
105 },
106 want: &metav1.Table{
107 ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
108 Rows: []metav1.TableRow{
109 {
110 Cells: []interface{}{"blah"},
111 Object: runtime.RawExtension{
112 Object: &metav1beta1.PartialObjectMetadata{
113 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
114 },
115 },
116 },
117 },
118 },
119 },
120 {
121 name: "Return table for list",
122 fields: fields{
123 headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
124 },
125 args: args{
126 obj: &metav1beta1.PartialObjectMetadataList{
127 Items: []metav1beta1.PartialObjectMetadata{
128 {ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))}},
129 {ObjectMeta: metav1.ObjectMeta{Name: "blah-2", CreationTimestamp: metav1.NewTime(time.Unix(2, 0))}},
130 },
131 },
132 tableOptions: nil,
133 },
134 want: &metav1.Table{
135 ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
136 Rows: []metav1.TableRow{
137 {
138 Cells: []interface{}{"blah"},
139 Object: runtime.RawExtension{
140 Object: &metav1beta1.PartialObjectMetadata{
141 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
142 },
143 },
144 },
145 {
146 Cells: []interface{}{"blah-2"},
147 Object: runtime.RawExtension{
148 Object: &metav1beta1.PartialObjectMetadata{
149 ObjectMeta: metav1.ObjectMeta{Name: "blah-2", CreationTimestamp: metav1.NewTime(time.Unix(2, 0))},
150 },
151 },
152 },
153 },
154 },
155 },
156 {
157 name: "Accept TableOptions",
158 fields: fields{
159 headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
160 },
161 args: args{
162 obj: &metav1beta1.PartialObjectMetadata{
163 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
164 },
165 tableOptions: &metav1.TableOptions{},
166 },
167 want: &metav1.Table{
168 ColumnDefinitions: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
169 Rows: []metav1.TableRow{
170 {
171 Cells: []interface{}{"blah"},
172 Object: runtime.RawExtension{
173 Object: &metav1beta1.PartialObjectMetadata{
174 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
175 },
176 },
177 },
178 },
179 },
180 },
181 {
182 name: "Omit headers from TableOptions",
183 fields: fields{
184 headers: []metav1.TableColumnDefinition{{Name: "name", Type: "string"}},
185 },
186 args: args{
187 obj: &metav1beta1.PartialObjectMetadata{
188 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
189 },
190 tableOptions: &metav1.TableOptions{NoHeaders: true},
191 },
192 want: &metav1.Table{
193 Rows: []metav1.TableRow{
194 {
195 Cells: []interface{}{"blah"},
196 Object: runtime.RawExtension{
197 Object: &metav1beta1.PartialObjectMetadata{
198 ObjectMeta: metav1.ObjectMeta{Name: "blah", CreationTimestamp: metav1.NewTime(time.Unix(1, 0))},
199 },
200 },
201 },
202 },
203 },
204 },
205 {
206 name: "Return table with additional column containing multiple string values",
207 fields: fields{
208 headers: []metav1.TableColumnDefinition{
209 {Name: "name", Type: "string"},
210 {Name: "valueOnly", Type: "string"},
211 {Name: "single1", Type: "string"},
212 {Name: "single2", Type: "string"},
213 {Name: "multi", Type: "string"},
214 },
215 additionalColumns: []columnPrinter{
216 newJSONPath("valueOnly", "{.spec.servers[0].hosts[0]}"),
217 newJSONPath("single1", "{.spec.servers[0].hosts}"),
218 newJSONPath("single2", "{.spec.servers[1].hosts}"),
219 newJSONPath("multi", "{.spec.servers[*].hosts}"),
220 },
221 },
222 args: args{
223 obj: &unstructured.Unstructured{
224 Object: map[string]interface{}{
225 "apiVersion": "example.istio.io/v1alpha1",
226 "kind": "Blah",
227 "metadata": map[string]interface{}{
228 "name": "blah",
229 },
230 "spec": map[string]interface{}{
231 "servers": []map[string]interface{}{
232 {"hosts": []string{"foo"}},
233 {"hosts": []string{"bar", "baz"}},
234 },
235 },
236 },
237 },
238 tableOptions: nil,
239 },
240 want: &metav1.Table{
241 ColumnDefinitions: []metav1.TableColumnDefinition{
242 {Name: "name", Type: "string"},
243 {Name: "valueOnly", Type: "string"},
244 {Name: "single1", Type: "string"},
245 {Name: "single2", Type: "string"},
246 {Name: "multi", Type: "string"},
247 },
248 Rows: []metav1.TableRow{
249 {
250 Cells: []interface{}{
251 "blah",
252 "foo",
253 `["foo"]`,
254 `["bar","baz"]`,
255 `["foo"]`,
256 },
257 Object: runtime.RawExtension{
258 Object: &unstructured.Unstructured{
259 Object: map[string]interface{}{
260 "apiVersion": "example.istio.io/v1alpha1",
261 "kind": "Blah",
262 "metadata": map[string]interface{}{
263 "name": "blah",
264 },
265 "spec": map[string]interface{}{
266 "servers": []map[string]interface{}{
267 {"hosts": []string{"foo"}},
268 {"hosts": []string{"bar", "baz"}},
269 },
270 },
271 },
272 },
273 },
274 },
275 },
276 },
277 },
278 {
279 name: "Return table with additional column containing multiple integer values as string",
280 fields: fields{
281 headers: []metav1.TableColumnDefinition{
282 {Name: "name", Type: "string"},
283 {Name: "valueOnly", Type: "string"},
284 {Name: "single1", Type: "string"},
285 {Name: "single2", Type: "string"},
286 {Name: "multi", Type: "string"},
287 },
288 additionalColumns: []columnPrinter{
289 newJSONPath("valueOnly", "{.spec.foo[0].bar[0]}"),
290 newJSONPath("single1", "{.spec.foo[0].bar}"),
291 newJSONPath("single2", "{.spec.foo[1].bar}"),
292 newJSONPath("multi", "{.spec.foo[*].bar}"),
293 },
294 },
295 args: args{
296 obj: &unstructured.Unstructured{
297 Object: map[string]interface{}{
298 "apiVersion": "example.istio.io/v1alpha1",
299 "kind": "Blah",
300 "metadata": map[string]interface{}{
301 "name": "blah",
302 },
303 "spec": map[string]interface{}{
304 "foo": []map[string]interface{}{
305 {"bar": []int64{1}},
306 {"bar": []int64{2, 3}},
307 },
308 },
309 },
310 },
311 tableOptions: nil,
312 },
313 want: &metav1.Table{
314 ColumnDefinitions: []metav1.TableColumnDefinition{
315 {Name: "name", Type: "string"},
316 {Name: "valueOnly", Type: "string"},
317 {Name: "single1", Type: "string"},
318 {Name: "single2", Type: "string"},
319 {Name: "multi", Type: "string"},
320 },
321 Rows: []metav1.TableRow{
322 {
323 Cells: []interface{}{
324 "blah",
325 "1",
326 "[1]",
327 "[2,3]",
328 "[1]",
329 },
330 Object: runtime.RawExtension{
331 Object: &unstructured.Unstructured{
332 Object: map[string]interface{}{
333 "apiVersion": "example.istio.io/v1alpha1",
334 "kind": "Blah",
335 "metadata": map[string]interface{}{
336 "name": "blah",
337 },
338 "spec": map[string]interface{}{
339 "foo": []map[string]interface{}{
340 {"bar": []int64{1}},
341 {"bar": []int64{2, 3}},
342 },
343 },
344 },
345 },
346 },
347 },
348 },
349 },
350 },
351 {
352 name: "Return table with additional column containing multiple integer values",
353 fields: fields{
354 headers: []metav1.TableColumnDefinition{
355 {Name: "name", Type: "string"},
356 {Name: "valueOnly", Type: "integer"},
357 {Name: "single1", Type: "integer"},
358 {Name: "single2", Type: "integer"},
359 {Name: "multi", Type: "integer"},
360 },
361 additionalColumns: []columnPrinter{
362 newJSONPath("valueOnly", "{.spec.foo[0].bar[0]}"),
363 newJSONPath("single1", "{.spec.foo[0].bar}"),
364 newJSONPath("single2", "{.spec.foo[1].bar}"),
365 newJSONPath("multi", "{.spec.foo[*].bar}"),
366 },
367 },
368 args: args{
369 obj: &unstructured.Unstructured{
370 Object: map[string]interface{}{
371 "apiVersion": "example.istio.io/v1alpha1",
372 "kind": "Blah",
373 "metadata": map[string]interface{}{
374 "name": "blah",
375 },
376 "spec": map[string]interface{}{
377 "foo": []map[string]interface{}{
378 {"bar": []int64{1}},
379 {"bar": []int64{2, 3}},
380 },
381 },
382 },
383 },
384 tableOptions: nil,
385 },
386 want: &metav1.Table{
387 ColumnDefinitions: []metav1.TableColumnDefinition{
388 {Name: "name", Type: "string"},
389 {Name: "valueOnly", Type: "integer"},
390 {Name: "single1", Type: "integer"},
391 {Name: "single2", Type: "integer"},
392 {Name: "multi", Type: "integer"},
393 },
394 Rows: []metav1.TableRow{
395 {
396 Cells: []interface{}{
397 "blah",
398 int64(1),
399 nil,
400 nil,
401 nil,
402 },
403 Object: runtime.RawExtension{
404 Object: &unstructured.Unstructured{
405 Object: map[string]interface{}{
406 "apiVersion": "example.istio.io/v1alpha1",
407 "kind": "Blah",
408 "metadata": map[string]interface{}{
409 "name": "blah",
410 },
411 "spec": map[string]interface{}{
412 "foo": []map[string]interface{}{
413 {"bar": []int64{1}},
414 {"bar": []int64{2, 3}},
415 },
416 },
417 },
418 },
419 },
420 },
421 },
422 },
423 },
424 }
425 for _, tt := range tests {
426 t.Run(tt.name, func(t *testing.T) {
427 c := &convertor{
428 headers: tt.fields.headers,
429 additionalColumns: tt.fields.additionalColumns,
430 }
431 got, err := c.ConvertToTable(tt.args.ctx, tt.args.obj, tt.args.tableOptions)
432 if (err != nil) != tt.wantErr {
433 t.Errorf("convertor.ConvertToTable() error = %v, wantErr %v", err, tt.wantErr)
434 return
435 }
436 if !reflect.DeepEqual(got, tt.want) {
437 t.Errorf("convertor.ConvertToTable() = %s", cmp.Diff(tt.want, got))
438 }
439 })
440 }
441 }
442
443 func newJSONPath(name string, jsonPathExpression string) columnPrinter {
444 jp := jsonpath.New(name)
445 _ = jp.Parse(jsonPathExpression)
446 return jp
447 }
448
View as plain text