1
16
17 package pruning
18
19 import (
20 "bytes"
21 "reflect"
22 "strings"
23 "testing"
24
25 "github.com/google/go-cmp/cmp"
26
27 "k8s.io/apimachinery/pkg/runtime"
28 "k8s.io/apimachinery/pkg/util/json"
29
30 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
31 )
32
33 func TestPrune(t *testing.T) {
34 tests := []struct {
35 name string
36 json string
37 isResourceRoot bool
38 schema *structuralschema.Structural
39 expectedObject string
40 expectedPruned []string
41 }{
42 {name: "empty", json: "null", expectedObject: "null"},
43 {name: "scalar", json: "4", schema: &structuralschema.Structural{}, expectedObject: "4"},
44 {name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
45 Items: &structuralschema.Structural{},
46 }, expectedObject: "[1,2]"},
47 {name: "object array", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, schema: &structuralschema.Structural{
48 Items: &structuralschema.Structural{
49 Properties: map[string]structuralschema.Structural{
50 "a": {},
51 "c": {},
52 },
53 },
54 }, expectedObject: `[{"a":1},{},{"a":1,"c":3}]`, expectedPruned: []string{"[1].b", "[2].b"}},
55 {name: "object array with nil schema", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, expectedObject: `[{},{},{}]`,
56 expectedPruned: []string{"[0].a", "[1].b", "[2].a", "[2].b", "[2].c"}},
57 {name: "object array object", json: `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, schema: &structuralschema.Structural{
58 Properties: map[string]structuralschema.Structural{
59 "array": {
60 Items: &structuralschema.Structural{
61 Properties: map[string]structuralschema.Structural{
62 "a": {},
63 "c": {},
64 },
65 },
66 },
67 "specified": {
68 Properties: map[string]structuralschema.Structural{
69 "a": {},
70 "c": {},
71 },
72 },
73 },
74 }, expectedObject: `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`,
75 expectedPruned: []string{"array[1].b", "array[2].b", "specified.b", "unspecified"}},
76 {name: "nested x-kubernetes-preserve-unknown-fields", json: `
77 {
78 "unspecified":"bar",
79 "alpha": "abc",
80 "beta": 42.0,
81 "unspecifiedObject": {"unspecified": "bar"},
82 "pruning": {
83 "unspecified": "bar",
84 "unspecifiedObject": {"unspecified": "bar"},
85 "pruning": {"unspecified": "bar"},
86 "apiVersion": "unknown",
87 "preserving": {"unspecified": "bar"}
88 },
89 "preserving": {
90 "unspecified": "bar",
91 "unspecifiedObject": {"unspecified": "bar"},
92 "pruning": {"unspecified": "bar"},
93 "preserving": {"unspecified": "bar"},
94 "preservingUnknownType": [{"foo":true},{"bar":true}]
95 },
96 "preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields": {
97 "foo": {
98 "specified": {"unspecified":"bar"},
99 "unspecified": "bar"
100 }
101 },
102 "preservingAdditionalPropertiesKeyPruneValues": {
103 "foo": {
104 "specified": {"unspecified":"bar"},
105 "unspecified": "bar"
106 }
107 }
108 }
109 `, schema: &structuralschema.Structural{
110 Generic: structuralschema.Generic{Type: "object"},
111 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
112 Properties: map[string]structuralschema.Structural{
113 "alpha": {Generic: structuralschema.Generic{Type: "string"}},
114 "beta": {Generic: structuralschema.Generic{Type: "number"}},
115 "pruning": {
116 Generic: structuralschema.Generic{Type: "object"},
117 Properties: map[string]structuralschema.Structural{
118 "preserving": {
119 Generic: structuralschema.Generic{Type: "object"},
120 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
121 },
122 "pruning": {
123 Generic: structuralschema.Generic{Type: "object"},
124 },
125 },
126 },
127 "preserving": {
128 Generic: structuralschema.Generic{Type: "object"},
129 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
130 Properties: map[string]structuralschema.Structural{
131 "preserving": {
132 Generic: structuralschema.Generic{Type: "object"},
133 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
134 },
135 "preservingUnknownType": {
136 Generic: structuralschema.Generic{Type: ""},
137 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
138 },
139 "pruning": {
140 Generic: structuralschema.Generic{Type: "object"},
141 },
142 },
143 },
144 "preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields": {
145
146 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
147 Generic: structuralschema.Generic{
148 Type: "object",
149 AdditionalProperties: &structuralschema.StructuralOrBool{
150 Structural: &structuralschema.Structural{
151 Generic: structuralschema.Generic{Type: "object"},
152 Properties: map[string]structuralschema.Structural{
153 "specified": {Generic: structuralschema.Generic{Type: "object"}},
154 },
155 },
156 },
157 },
158 },
159 "preservingAdditionalPropertiesKeyPruneValues": {
160 Generic: structuralschema.Generic{
161 Type: "object",
162 AdditionalProperties: &structuralschema.StructuralOrBool{
163 Structural: &structuralschema.Structural{
164 Generic: structuralschema.Generic{Type: "object"},
165 Properties: map[string]structuralschema.Structural{
166 "specified": {Generic: structuralschema.Generic{Type: "object"}},
167 },
168 },
169 },
170 },
171 },
172 },
173 }, expectedObject: `
174 {
175 "unspecified":"bar",
176 "alpha": "abc",
177 "beta": 42.0,
178 "unspecifiedObject": {"unspecified": "bar"},
179 "pruning": {
180 "pruning": {},
181 "preserving": {"unspecified": "bar"}
182 },
183 "preserving": {
184 "unspecified": "bar",
185 "unspecifiedObject": {"unspecified": "bar"},
186 "pruning": {},
187 "preserving": {"unspecified": "bar"},
188 "preservingUnknownType": [{"foo":true},{"bar":true}]
189 },
190 "preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields": {
191 "foo": {
192 "specified": {}
193 }
194 },
195 "preservingAdditionalPropertiesKeyPruneValues": {
196 "foo": {
197 "specified": {}
198 }
199 }
200 }
201 `, expectedPruned: []string{"preserving.pruning.unspecified", "preservingAdditionalPropertiesKeyPruneValues.foo.specified.unspecified", "preservingAdditionalPropertiesKeyPruneValues.foo.unspecified", "preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields.foo.specified.unspecified", "preservingAdditionalPropertiesNotInheritingXPreserveUnknownFields.foo.unspecified", "pruning.apiVersion", "pruning.pruning.unspecified", "pruning.unspecified", "pruning.unspecifiedObject"}},
202 {name: "additionalProperties with schema", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{
203 Properties: map[string]structuralschema.Structural{
204 "a": {},
205 "c": {
206 Generic: structuralschema.Generic{
207 AdditionalProperties: &structuralschema.StructuralOrBool{
208 Structural: &structuralschema.Structural{
209 Generic: structuralschema.Generic{
210 Type: "integer",
211 },
212 },
213 },
214 },
215 },
216 },
217 }, expectedObject: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`,
218 expectedPruned: []string{"b", "c.c.a"}},
219 {name: "additionalProperties with bool", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1, "apiVersion": "unknown"}}}`, schema: &structuralschema.Structural{
220 Properties: map[string]structuralschema.Structural{
221 "a": {},
222 "c": {
223 Generic: structuralschema.Generic{
224 AdditionalProperties: &structuralschema.StructuralOrBool{
225 Bool: false,
226 },
227 },
228 },
229 },
230 }, expectedObject: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`,
231 expectedPruned: []string{"b", "c.c.a", "c.c.apiVersion"}},
232 {name: "x-kubernetes-embedded-resource", json: `
233 {
234 "apiVersion": "foo/v1",
235 "kind": "Foo",
236 "metadata": {
237 "name": "instance",
238 "unspecified": "bar"
239 },
240 "unspecified":"bar",
241 "pruned": {
242 "apiVersion": "foo/v1",
243 "kind": "Foo",
244 "unspecified": "bar",
245 "metadata": {
246 "name": "instance",
247 "unspecified": "bar"
248 },
249 "spec": {
250 "unspecified": "bar"
251 }
252 },
253 "preserving": {
254 "apiVersion": "foo/v1",
255 "kind": "Foo",
256 "unspecified": "bar",
257 "metadata": {
258 "name": "instance",
259 "unspecified": "bar"
260 },
261 "spec": {
262 "unspecified": "bar"
263 }
264 },
265 "nested": {
266 "apiVersion": "foo/v1",
267 "kind": "Foo",
268 "unspecified": "bar",
269 "metadata": {
270 "name": "instance",
271 "unspecified": "bar"
272 },
273 "spec": {
274 "unspecified": "bar",
275 "embedded": {
276 "apiVersion": "foo/v1",
277 "kind": "Foo",
278 "unspecified": "bar",
279 "metadata": {
280 "name": "instance",
281 "unspecified": "bar"
282 },
283 "spec": {
284 "unspecified": "bar"
285 }
286 }
287 }
288 }
289 }
290 `, schema: &structuralschema.Structural{
291 Generic: structuralschema.Generic{Type: "object"},
292 Properties: map[string]structuralschema.Structural{
293 "pruned": {
294 Generic: structuralschema.Generic{Type: "object"},
295 Extensions: structuralschema.Extensions{
296 XEmbeddedResource: true,
297 },
298 Properties: map[string]structuralschema.Structural{
299 "spec": {
300 Generic: structuralschema.Generic{Type: "object"},
301 },
302 },
303 },
304 "preserving": {
305 Generic: structuralschema.Generic{Type: "object"},
306 Extensions: structuralschema.Extensions{
307 XEmbeddedResource: true,
308 XPreserveUnknownFields: true,
309 },
310 },
311 "nested": {
312 Generic: structuralschema.Generic{Type: "object"},
313 Extensions: structuralschema.Extensions{
314 XEmbeddedResource: true,
315 },
316 Properties: map[string]structuralschema.Structural{
317 "spec": {
318 Generic: structuralschema.Generic{Type: "object"},
319 Properties: map[string]structuralschema.Structural{
320 "embedded": {
321 Generic: structuralschema.Generic{Type: "object"},
322 Extensions: structuralschema.Extensions{
323 XEmbeddedResource: true,
324 },
325 Properties: map[string]structuralschema.Structural{
326 "spec": {
327 Generic: structuralschema.Generic{Type: "object"},
328 },
329 },
330 },
331 },
332 },
333 },
334 },
335 },
336 }, expectedObject: `
337 {
338 "pruned": {
339 "apiVersion": "foo/v1",
340 "kind": "Foo",
341 "metadata": {
342 "name": "instance",
343 "unspecified": "bar"
344 },
345 "spec": {
346 }
347 },
348 "preserving": {
349 "apiVersion": "foo/v1",
350 "kind": "Foo",
351 "unspecified": "bar",
352 "metadata": {
353 "name": "instance",
354 "unspecified": "bar"
355 },
356 "spec": {
357 "unspecified": "bar"
358 }
359 },
360 "nested": {
361 "apiVersion": "foo/v1",
362 "kind": "Foo",
363 "metadata": {
364 "name": "instance",
365 "unspecified": "bar"
366 },
367 "spec": {
368 "embedded": {
369 "apiVersion": "foo/v1",
370 "kind": "Foo",
371 "metadata": {
372 "name": "instance",
373 "unspecified": "bar"
374 },
375 "spec": {
376 }
377 }
378 }
379 }
380 }
381 `, expectedPruned: []string{"nested.spec.embedded.spec.unspecified", "nested.spec.embedded.unspecified", "nested.spec.unspecified", "nested.unspecified", "pruned.spec.unspecified", "pruned.unspecified", "unspecified"}},
382 {name: "x-kubernetes-embedded-resource, with root=true", json: `
383 {
384 "apiVersion": "foo/v1",
385 "kind": "Foo",
386 "metadata": {
387 "name": "instance",
388 "namespace": "myns",
389 "labels":{"foo":"bar"},
390 "unspecified": "bar"
391 },
392 "unspecified":"bar",
393 "pruned": {
394 "apiVersion": "foo/v1",
395 "kind": "Foo",
396 "unspecified": "bar",
397 "metadata": {
398 "name": "instance",
399 "namespace": "myns",
400 "labels":{"foo":"bar"},
401 "unspecified": "bar"
402 },
403 "spec": {
404 "unspecified": "bar"
405 }
406 },
407 "preserving": {
408 "apiVersion": "foo/v1",
409 "kind": "Foo",
410 "unspecified": "bar",
411 "metadata": {
412 "name": "instance",
413 "namespace": "myns",
414 "labels":{"foo":"bar"},
415 "unspecified": "bar"
416 },
417 "spec": {
418 "unspecified": "bar"
419 }
420 },
421 "nested": {
422 "apiVersion": "foo/v1",
423 "kind": "Foo",
424 "unspecified": "bar",
425 "metadata": {
426 "name": "instance",
427 "namespace": "myns",
428 "labels":{"foo":"bar"},
429 "unspecified": "bar"
430 },
431 "spec": {
432 "unspecified": "bar",
433 "embedded": {
434 "apiVersion": "foo/v1",
435 "kind": "Foo",
436 "unspecified": "bar",
437 "metadata": {
438 "name": "instance",
439 "namespace": "myns",
440 "labels":{"foo":"bar"},
441 "unspecified": "bar"
442 },
443 "spec": {
444 "unspecified": "bar"
445 }
446 }
447 }
448 }
449 }
450 `, isResourceRoot: true, schema: &structuralschema.Structural{
451 Generic: structuralschema.Generic{Type: "object"},
452 Properties: map[string]structuralschema.Structural{
453 "metadata": {
454 Generic: structuralschema.Generic{Type: "object"},
455 },
456 "pruned": {
457 Generic: structuralschema.Generic{Type: "object"},
458 Extensions: structuralschema.Extensions{
459 XEmbeddedResource: true,
460 },
461 Properties: map[string]structuralschema.Structural{
462 "metadata": {
463 Generic: structuralschema.Generic{Type: "object"},
464 },
465 "spec": {
466 Generic: structuralschema.Generic{Type: "object"},
467 },
468 },
469 },
470 "preserving": {
471 Generic: structuralschema.Generic{Type: "object"},
472 Extensions: structuralschema.Extensions{
473 XEmbeddedResource: true,
474 XPreserveUnknownFields: true,
475 },
476 },
477 "nested": {
478 Generic: structuralschema.Generic{Type: "object"},
479 Extensions: structuralschema.Extensions{
480 XEmbeddedResource: true,
481 },
482 Properties: map[string]structuralschema.Structural{
483 "spec": {
484 Generic: structuralschema.Generic{Type: "object"},
485 Properties: map[string]structuralschema.Structural{
486 "embedded": {
487 Generic: structuralschema.Generic{Type: "object"},
488 Extensions: structuralschema.Extensions{
489 XEmbeddedResource: true,
490 },
491 Properties: map[string]structuralschema.Structural{
492 "metadata": {
493 Generic: structuralschema.Generic{Type: "object"},
494 },
495 "spec": {
496 Generic: structuralschema.Generic{Type: "object"},
497 },
498 },
499 },
500 },
501 },
502 },
503 },
504 },
505 }, expectedObject: `
506 {
507 "apiVersion": "foo/v1",
508 "kind": "Foo",
509 "metadata": {
510 "name": "instance",
511 "namespace": "myns",
512 "labels": {"foo": "bar"},
513 "unspecified": "bar"
514 },
515 "pruned": {
516 "apiVersion": "foo/v1",
517 "kind": "Foo",
518 "metadata": {
519 "name": "instance",
520 "namespace": "myns",
521 "labels": {"foo": "bar"},
522 "unspecified": "bar"
523 },
524 "spec": {
525 }
526 },
527 "preserving": {
528 "apiVersion": "foo/v1",
529 "kind": "Foo",
530 "unspecified": "bar",
531 "metadata": {
532 "name": "instance",
533 "namespace": "myns",
534 "labels": {"foo": "bar"},
535 "unspecified": "bar"
536 },
537 "spec": {
538 "unspecified": "bar"
539 }
540 },
541 "nested": {
542 "apiVersion": "foo/v1",
543 "kind": "Foo",
544 "metadata": {
545 "name": "instance",
546 "namespace": "myns",
547 "labels": {"foo": "bar"},
548 "unspecified": "bar"
549 },
550 "spec": {
551 "embedded": {
552 "apiVersion": "foo/v1",
553 "kind": "Foo",
554 "metadata": {
555 "name": "instance",
556 "namespace": "myns",
557 "labels": {"foo": "bar"},
558 "unspecified": "bar"
559 },
560 "spec": {
561 }
562 }
563 }
564 }
565 }
566 `, expectedPruned: []string{"nested.spec.embedded.spec.unspecified", "nested.spec.embedded.unspecified", "nested.spec.unspecified", "nested.unspecified", "pruned.spec.unspecified", "pruned.unspecified", "unspecified"}},
567 }
568 for _, tt := range tests {
569 t.Run(tt.name, func(t *testing.T) {
570 var in interface{}
571 if err := json.Unmarshal([]byte(tt.json), &in); err != nil {
572 t.Fatal(err)
573 }
574
575 var expectedObject interface{}
576 if err := json.Unmarshal([]byte(tt.expectedObject), &expectedObject); err != nil {
577 t.Fatal(err)
578 }
579
580 pruned := PruneWithOptions(in, tt.schema, tt.isResourceRoot, structuralschema.UnknownFieldPathOptions{
581 TrackUnknownFieldPaths: true,
582 })
583 if !reflect.DeepEqual(in, expectedObject) {
584 var buf bytes.Buffer
585 enc := json.NewEncoder(&buf)
586 enc.SetIndent("", " ")
587 err := enc.Encode(in)
588 if err != nil {
589 t.Fatalf("unexpected result mashalling error: %v", err)
590 }
591 t.Errorf("expected object: %s\ngot: %s\ndiff: %s", tt.expectedObject, buf.String(), cmp.Diff(expectedObject, in))
592 }
593 if !reflect.DeepEqual(pruned, tt.expectedPruned) {
594 t.Errorf("expected pruned:\n\t%v\ngot:\n\t%v\n", strings.Join(tt.expectedPruned, "\n\t"), strings.Join(pruned, "\n\t"))
595 }
596
597
598 emptyPruned := PruneWithOptions(in, tt.schema, tt.isResourceRoot, structuralschema.UnknownFieldPathOptions{})
599 if !reflect.DeepEqual(in, expectedObject) {
600 var buf bytes.Buffer
601 enc := json.NewEncoder(&buf)
602 enc.SetIndent("", " ")
603 err := enc.Encode(in)
604 if err != nil {
605 t.Fatalf("unexpected result mashalling error: %v", err)
606 }
607 t.Errorf("expected object: %s\ngot: %s\ndiff: %s", tt.expectedObject, buf.String(), cmp.Diff(expectedObject, in))
608 }
609 if len(emptyPruned) > 0 {
610 t.Errorf("unexpectedly returned pruned fields: %v", emptyPruned)
611 }
612 })
613 }
614 }
615
616 const smallInstance = `
617 {
618 "unspecified":"bar",
619 "alpha": "abc",
620 "beta": 42.0,
621 "unspecifiedObject": {"unspecified": "bar"},
622 "pruning": {
623 "pruning": {},
624 "preserving": {"unspecified": "bar"}
625 },
626 "preserving": {
627 "unspecified": "bar",
628 "unspecifiedObject": {"unspecified": "bar"},
629 "pruning": {},
630 "preserving": {"unspecified": "bar"}
631 }
632 }
633 `
634
635 func BenchmarkPrune(b *testing.B) {
636 b.StopTimer()
637 b.ReportAllocs()
638
639 schema := &structuralschema.Structural{
640 Generic: structuralschema.Generic{Type: "object"},
641 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
642 Properties: map[string]structuralschema.Structural{
643 "alpha": {Generic: structuralschema.Generic{Type: "string"}},
644 "beta": {Generic: structuralschema.Generic{Type: "number"}},
645 "pruning": {
646 Generic: structuralschema.Generic{Type: "object"},
647 Properties: map[string]structuralschema.Structural{
648 "preserving": {
649 Generic: structuralschema.Generic{Type: "object"},
650 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
651 },
652 "pruning": {
653 Generic: structuralschema.Generic{Type: "object"},
654 },
655 },
656 },
657 "preserving": {
658 Generic: structuralschema.Generic{Type: "object"},
659 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
660 Properties: map[string]structuralschema.Structural{
661 "preserving": {
662 Generic: structuralschema.Generic{Type: "object"},
663 Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
664 },
665 "pruning": {
666 Generic: structuralschema.Generic{Type: "object"},
667 },
668 },
669 },
670 },
671 }
672
673 var obj map[string]interface{}
674 err := json.Unmarshal([]byte(smallInstance), &obj)
675 if err != nil {
676 b.Fatal(err)
677 }
678
679 instances := make([]map[string]interface{}, 0, b.N)
680 for i := 0; i < b.N; i++ {
681 instances = append(instances, runtime.DeepCopyJSON(obj))
682 }
683
684 b.StartTimer()
685 for i := 0; i < b.N; i++ {
686 Prune(instances[i], schema, true)
687 }
688 }
689
690 func BenchmarkDeepCopy(b *testing.B) {
691 b.StopTimer()
692 b.ReportAllocs()
693
694 var obj map[string]interface{}
695 err := json.Unmarshal([]byte(smallInstance), &obj)
696 if err != nil {
697 b.Fatal(err)
698 }
699
700 instances := make([]map[string]interface{}, 0, b.N)
701
702 b.StartTimer()
703 for i := 0; i < b.N; i++ {
704
705 instances = append(instances, runtime.DeepCopyJSON(obj))
706 }
707 }
708
709 func BenchmarkUnmarshal(b *testing.B) {
710 b.StopTimer()
711 b.ReportAllocs()
712
713 instances := make([]map[string]interface{}, b.N)
714
715 b.StartTimer()
716 for i := 0; i < b.N; i++ {
717 err := json.Unmarshal([]byte(smallInstance), &instances[i])
718 if err != nil {
719 b.Fatal(err)
720 }
721 }
722 }
723
View as plain text