1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package k8s_test
16
17 import (
18 "encoding/json"
19 "testing"
20
21 corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
22 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/deepcopy"
23 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
24 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
25 testk8s "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/k8s"
26
27 apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "sigs.k8s.io/structured-merge-diff/v4/fieldpath"
30 )
31
32 var emptyObject = make(map[string]interface{})
33
34 func TestConstructManagedFieldSet(t *testing.T) {
35 tests := []struct {
36 name string
37 managedFieldEntries []v1.ManagedFieldsEntry
38 expectedSet *fieldpath.Set
39 }{
40 {
41 name: "fields from separate managers are combined",
42 managedFieldEntries: []v1.ManagedFieldsEntry{
43 mapToManagedFieldEntry(t, "managerA", map[string]interface{}{
44 "f:spec": map[string]interface{}{
45 ".": emptyObject,
46 "f:simpleFieldA": emptyObject,
47 "f:nestedObjectA": map[string]interface{}{
48 ".": emptyObject,
49 "f:nestedFieldA": emptyObject,
50 },
51 },
52 }),
53 mapToManagedFieldEntry(t, "managerB", map[string]interface{}{
54 "f:spec": map[string]interface{}{
55 ".": emptyObject,
56 "f:simpleFieldB": emptyObject,
57 "f:nestedObjectB": map[string]interface{}{
58 ".": emptyObject,
59 "f:nestedFieldB": emptyObject,
60 },
61 },
62 }),
63 },
64 expectedSet: testk8s.MapToFieldPathSet(t, map[string]interface{}{
65 "f:simpleFieldA": emptyObject,
66 "f:simpleFieldB": emptyObject,
67 "f:nestedObjectA": map[string]interface{}{
68 ".": emptyObject,
69 "f:nestedFieldA": emptyObject,
70 },
71 "f:nestedObjectB": map[string]interface{}{
72 ".": emptyObject,
73 "f:nestedFieldB": emptyObject,
74 },
75 }),
76 },
77 {
78 name: "fields from the cnrm-controller-manager manager are ignored",
79 managedFieldEntries: []v1.ManagedFieldsEntry{
80 mapToManagedFieldEntry(t, "managerA", map[string]interface{}{
81 "f:spec": map[string]interface{}{
82 ".": emptyObject,
83 "f:simpleFieldA": emptyObject,
84 "f:nestedObjectA": map[string]interface{}{
85 ".": emptyObject,
86 "f:nestedFieldA": emptyObject,
87 },
88 },
89 }),
90 mapToManagedFieldEntry(t, k8s.ControllerManagedFieldManager, map[string]interface{}{
91 "f:spec": map[string]interface{}{
92 ".": emptyObject,
93 "f:simpleFieldKCC": emptyObject,
94 "f:nestedObjectKCC": map[string]interface{}{
95 ".": emptyObject,
96 "f:nestedFieldKCC": emptyObject,
97 },
98 },
99 }),
100 },
101 expectedSet: testk8s.MapToFieldPathSet(t, map[string]interface{}{
102 "f:simpleFieldA": emptyObject,
103 "f:nestedObjectA": map[string]interface{}{
104 ".": emptyObject,
105 "f:nestedFieldA": emptyObject,
106 },
107 }),
108 },
109 }
110 for _, tc := range tests {
111 t.Run(tc.name, func(t *testing.T) {
112 managedFieldSet, err := k8s.ConstructManagedFieldsV1Set(tc.managedFieldEntries)
113 if err != nil {
114 t.Error("error constructing managed field set:", err)
115 return
116 }
117 if !managedFieldSet.Equals(tc.expectedSet) {
118 t.Errorf("actual and expected sets do not match: actual: %v, expected: %v",
119 string(fieldPathSetToJSON(t, managedFieldSet)),
120 string(fieldPathSetToJSON(t, tc.expectedSet)))
121 return
122 }
123 })
124 }
125 }
126
127 var schema = &apiextensions.JSONSchemaProps{
128 Properties: map[string]apiextensions.JSONSchemaProps{
129 "spec": {
130 Properties: map[string]apiextensions.JSONSchemaProps{
131 "field": {Type: "string"},
132 "external": {Type: "string"},
133 "unrelated": {Type: "string"},
134 "projectRef": {
135 Properties: map[string]apiextensions.JSONSchemaProps{
136 "external": {Type: "string"},
137 "name": {Type: "string"},
138 "namespace": {Type: "string"},
139 },
140 Type: "object",
141 },
142 "obj": {
143 Properties: map[string]apiextensions.JSONSchemaProps{
144 "field": {Type: "string"},
145 "external": {Type: "string"},
146 "nestedObj": {
147 Properties: map[string]apiextensions.JSONSchemaProps{
148 "field": {Type: "string"},
149 "external": {Type: "string"},
150 },
151 Type: "object",
152 },
153 "nestedList": {
154 Items: &apiextensions.JSONSchemaPropsOrArray{
155 Schema: &apiextensions.JSONSchemaProps{Type: "string"},
156 },
157 Type: "array",
158 },
159 },
160 Type: "object",
161 },
162 "objMap": {
163 AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
164 Schema: &apiextensions.JSONSchemaProps{Type: "string"},
165 },
166 Type: "object",
167 },
168 "objMapSchemaless": {
169 Type: "object",
170 },
171 "list": {
172 Items: &apiextensions.JSONSchemaPropsOrArray{
173 Schema: &apiextensions.JSONSchemaProps{Type: "string"},
174 },
175 Type: "array",
176 },
177 },
178 Type: "object",
179 },
180 },
181 }
182
183 func TestOverlayManagedFieldsOntoState(t *testing.T) {
184 tests := []struct {
185 name string
186 spec map[string]interface{}
187 krmState map[string]interface{}
188 managedFields *fieldpath.Set
189 hierarchicalRefs []corekccv1alpha1.HierarchicalReference
190 expected map[string]interface{}
191 }{
192 {
193 name: "use spec values for k8s-managed fields",
194 spec: map[string]interface{}{
195 "field": "k8s",
196 },
197 krmState: map[string]interface{}{
198 "field": "external",
199 },
200 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
201 "f:field": emptyObject,
202 }),
203 expected: map[string]interface{}{
204 "field": "k8s",
205 },
206 },
207 {
208 name: "use state values for externally-managed fields",
209 spec: map[string]interface{}{
210 "field": "k8s",
211 },
212 krmState: map[string]interface{}{
213 "field": "external",
214 },
215 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
216 "f:unrelated": emptyObject,
217 }),
218 expected: map[string]interface{}{
219 "field": "external",
220 },
221 },
222 {
223 name: "use spec values for k8s-managed nested fields",
224 spec: map[string]interface{}{
225 "obj": map[string]interface{}{
226 "field": "k8s",
227 },
228 },
229 krmState: map[string]interface{}{
230 "obj": map[string]interface{}{
231 "field": "external",
232 },
233 },
234 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
235 "f:obj": map[string]interface{}{
236 ".": emptyObject,
237 "f:field": emptyObject,
238 },
239 }),
240 expected: map[string]interface{}{
241 "obj": map[string]interface{}{
242 "field": "k8s",
243 },
244 },
245 },
246 {
247 name: "use state value for externally-managed nested fields",
248 spec: map[string]interface{}{
249 "obj": map[string]interface{}{
250 "field": "k8s",
251 },
252 },
253 krmState: map[string]interface{}{
254 "obj": map[string]interface{}{
255 "field": "external",
256 },
257 },
258 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
259 "f:unrelated": emptyObject,
260 }),
261 expected: map[string]interface{}{
262 "obj": map[string]interface{}{
263 "field": "external",
264 },
265 },
266 },
267 {
268 name: "top-level mixed management fields are merged",
269 spec: map[string]interface{}{
270 "field": "k8s",
271 "external": "k8s",
272 },
273 krmState: map[string]interface{}{
274 "field": "external",
275 "external": "external",
276 },
277 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
278 "f:field": emptyObject,
279 }),
280 expected: map[string]interface{}{
281 "field": "k8s",
282 "external": "external",
283 },
284 },
285 {
286 name: "nested mixed management fields are merged",
287 spec: map[string]interface{}{
288 "obj": map[string]interface{}{
289 "field": "k8s",
290 "external": "k8s",
291 },
292 },
293 krmState: map[string]interface{}{
294 "obj": map[string]interface{}{
295 "field": "external",
296 "external": "external",
297 },
298 },
299 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
300 "f:obj": map[string]interface{}{
301 ".": emptyObject,
302 "f:field": emptyObject,
303 },
304 }),
305 expected: map[string]interface{}{
306 "obj": map[string]interface{}{
307 "field": "k8s",
308 "external": "external",
309 },
310 },
311 },
312 {
313 name: "object map fields are merged",
314 spec: map[string]interface{}{
315 "objMap": map[string]interface{}{
316 "field": "k8s",
317 "external": "k8s",
318 },
319 },
320 krmState: map[string]interface{}{
321 "objMap": map[string]interface{}{
322 "field": "external",
323 "external": "external",
324 },
325 },
326 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
327 "f:objMap": map[string]interface{}{
328 "f:field": emptyObject,
329 },
330 }),
331 expected: map[string]interface{}{
332 "objMap": map[string]interface{}{
333 "field": "k8s",
334 "external": "external",
335 },
336 },
337 },
338 {
339 name: "schemaless object map fields are merged",
340 spec: map[string]interface{}{
341 "objMapSchemaless": map[string]interface{}{
342 "field": "k8s",
343 "external": "k8s",
344 },
345 },
346 krmState: map[string]interface{}{
347 "objMapSchemaless": map[string]interface{}{
348 "field": "external",
349 "external": "external",
350 },
351 },
352 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
353 "f:objMapSchemaless": map[string]interface{}{
354 "f:field": emptyObject,
355 },
356 }),
357 expected: map[string]interface{}{
358 "objMapSchemaless": map[string]interface{}{
359 "field": "k8s",
360 "external": "external",
361 },
362 },
363 },
364
365 {
366
367
368 name: "always use k8s value for lists set in spec",
369 spec: map[string]interface{}{
370 "list": []interface{}{"k8s-first", "k8s-second"},
371 },
372 krmState: map[string]interface{}{
373 "list": []interface{}{"external-first", "external-second"},
374 },
375 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
376 "f:unrelated": emptyObject,
377 }),
378 expected: map[string]interface{}{
379 "list": []interface{}{"k8s-first", "k8s-second"},
380 },
381 },
382 {
383 name: "use external value for lists not set in spec",
384 spec: map[string]interface{}{
385 "field": "k8s",
386 },
387 krmState: map[string]interface{}{
388 "field": "external",
389 "list": []interface{}{"external-first", "external-second"},
390 },
391 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
392 "f:field": emptyObject,
393 }),
394 expected: map[string]interface{}{
395 "field": "k8s",
396 "list": []interface{}{"external-first", "external-second"},
397 },
398 },
399 {
400
401 name: "empty spec and managed fields are supported",
402 spec: nil,
403 krmState: map[string]interface{}{
404 "field": "external",
405 },
406 managedFields: nil,
407 expected: map[string]interface{}{
408 "field": "external",
409 },
410 },
411 {
412 name: "empty state is supported",
413 spec: map[string]interface{}{
414 "field": "k8s",
415 },
416 krmState: nil,
417 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
418 "f:field": emptyObject,
419 }),
420 expected: map[string]interface{}{
421 "field": "k8s",
422 },
423 },
424 {
425 name: "externally-managed fields can be cleared",
426 spec: map[string]interface{}{
427 "field": "external",
428 "unrelated": "val",
429 },
430 krmState: map[string]interface{}{
431 "unrelated": "val",
432 },
433 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
434 "f:unrelated": emptyObject,
435 }),
436 expected: map[string]interface{}{
437 "unrelated": "val",
438 },
439 },
440 {
441 name: "hierarchical reference is preserved",
442 spec: map[string]interface{}{
443 "projectRef": map[string]interface{}{
444 "external": "project_id",
445 },
446 "unrelated": "val",
447 },
448 krmState: map[string]interface{}{
449 "unrelated": "val",
450 },
451 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
452 "f:unrelated": emptyObject,
453 "f:projectRef": map[string]interface{}{
454 ".": emptyObject,
455 "f:external": emptyObject,
456 },
457 }),
458 hierarchicalRefs: []corekccv1alpha1.HierarchicalReference{
459 {
460 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
461 Key: "projectRef",
462 },
463 },
464 expected: map[string]interface{}{
465 "projectRef": map[string]interface{}{
466 "external": "project_id",
467 },
468 "unrelated": "val",
469 },
470 },
471 {
472 name: "hierarchical reference is preserved even if not in managed fields as long as in spec",
473 spec: map[string]interface{}{
474 "projectRef": map[string]interface{}{
475 "external": "project_id",
476 },
477 "unrelated": "val",
478 },
479 krmState: map[string]interface{}{
480 "unrelated": "val",
481 },
482 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
483 "f:unrelated": emptyObject,
484 }),
485 hierarchicalRefs: []corekccv1alpha1.HierarchicalReference{
486 {
487 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
488 Key: "projectRef",
489 },
490 },
491 expected: map[string]interface{}{
492 "projectRef": map[string]interface{}{
493 "external": "project_id",
494 },
495 "unrelated": "val",
496 },
497 },
498 {
499 name: "no hierarchical reference in output config if none in spec",
500 spec: map[string]interface{}{
501 "unrelated": "val",
502 },
503 krmState: map[string]interface{}{
504 "unrelated": "val",
505 },
506 managedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
507 "f:unrelated": emptyObject,
508 }),
509 hierarchicalRefs: []corekccv1alpha1.HierarchicalReference{
510 {
511 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
512 Key: "projectRef",
513 },
514 },
515 expected: map[string]interface{}{
516 "unrelated": "val",
517 },
518 },
519 }
520 for _, tc := range tests {
521 t.Run(tc.name, func(t *testing.T) {
522 spec := deepcopy.MapStringInterface(tc.spec)
523 res, err := k8s.OverlayManagedFieldsOntoState(spec, tc.krmState, tc.managedFields, schema, tc.hierarchicalRefs)
524 if err != nil {
525 t.Error("error overlaying externally-managed fields:", err)
526 return
527 }
528 if !test.Equals(t, res, tc.expected) {
529 t.Errorf("actual: %+v, expected: %+v", res, tc.expected)
530 return
531 }
532 })
533 }
534 }
535
536 func TestConstructTrimmedSpecWithManagedFields(t *testing.T) {
537 tests := []struct {
538 name string
539 resource *k8s.Resource
540 hierarchicalRefs []corekccv1alpha1.HierarchicalReference
541 expected map[string]interface{}
542 }{
543 {
544 name: "no managed field information present",
545 resource: &k8s.Resource{
546 Spec: map[string]interface{}{
547 "field": "value",
548 "list": []interface{}{"a", "b"},
549 },
550 },
551 expected: map[string]interface{}{
552 "field": "value",
553 "list": []interface{}{"a", "b"},
554 },
555 },
556 {
557 name: "preserve k8s managed fields on the top level",
558 resource: &k8s.Resource{
559 Spec: map[string]interface{}{
560 "field": "k8s",
561 "external": "external",
562 },
563 ManagedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
564 "f:field": emptyObject,
565 }),
566 },
567 expected: map[string]interface{}{
568 "field": "k8s",
569 },
570 },
571 {
572 name: "preserve k8s-managed nested fields",
573 resource: &k8s.Resource{
574 Spec: map[string]interface{}{
575 "obj": map[string]interface{}{
576 "field": "k8s",
577 "nestedObj": map[string]interface{}{
578 "field": "k8s",
579 "external": "external",
580 },
581 "external": "external",
582 },
583 },
584 ManagedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
585 "f:obj": map[string]interface{}{
586 ".": emptyObject,
587 "f:field": emptyObject,
588 "f:nestedObj": map[string]interface{}{
589 ".": emptyObject,
590 "f:field": emptyObject,
591 },
592 },
593 }),
594 },
595 expected: map[string]interface{}{
596 "obj": map[string]interface{}{
597 "field": "k8s",
598 "nestedObj": map[string]interface{}{
599 "field": "k8s",
600 },
601 },
602 },
603 },
604 {
605
606
607 name: "always use k8s value for lists set in spec",
608 resource: &k8s.Resource{
609 Spec: map[string]interface{}{
610 "list": []interface{}{"k8s-first", "k8s-second"},
611 },
612 ManagedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
613 "f:unrelated": emptyObject,
614 }),
615 },
616 expected: map[string]interface{}{
617 "list": []interface{}{"k8s-first", "k8s-second"},
618 },
619 },
620 {
621 name: "empty spec and managed fields are supported",
622 resource: &k8s.Resource{},
623 expected: nil,
624 },
625 {
626 name: "hierarchical reference is preserved",
627 resource: &k8s.Resource{
628 Spec: map[string]interface{}{
629 "projectRef": map[string]interface{}{
630 "external": "project_id",
631 },
632 "unrelated": "val",
633 },
634 ManagedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
635 "f:unrelated": emptyObject,
636 "f:projectRef": map[string]interface{}{
637 ".": emptyObject,
638 "f:external": emptyObject,
639 },
640 }),
641 },
642 hierarchicalRefs: []corekccv1alpha1.HierarchicalReference{
643 {
644 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
645 Key: "projectRef",
646 },
647 },
648 expected: map[string]interface{}{
649 "projectRef": map[string]interface{}{
650 "external": "project_id",
651 },
652 "unrelated": "val",
653 },
654 },
655 {
656 name: "hierarchical reference is preserved even if not in managed fields as long as in spec",
657 resource: &k8s.Resource{
658 Spec: map[string]interface{}{
659 "projectRef": map[string]interface{}{
660 "external": "project_id",
661 },
662 "unrelated": "val",
663 },
664 ManagedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
665 "f:unrelated": emptyObject,
666 }),
667 },
668 hierarchicalRefs: []corekccv1alpha1.HierarchicalReference{
669 {
670 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
671 Key: "projectRef",
672 },
673 },
674 expected: map[string]interface{}{
675 "projectRef": map[string]interface{}{
676 "external": "project_id",
677 },
678 "unrelated": "val",
679 },
680 },
681 {
682 name: "no hierarchical reference in output config if none in spec",
683 resource: &k8s.Resource{
684 Spec: map[string]interface{}{
685 "unrelated": "val",
686 },
687 ManagedFields: testk8s.MapToFieldPathSet(t, map[string]interface{}{
688 "f:unrelated": emptyObject,
689 }),
690 },
691 hierarchicalRefs: []corekccv1alpha1.HierarchicalReference{
692 {
693 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
694 Key: "projectRef",
695 },
696 },
697 expected: map[string]interface{}{
698 "unrelated": "val",
699 },
700 },
701 }
702 for _, tc := range tests {
703 tc := tc
704 t.Run(tc.name, func(t *testing.T) {
705 t.Parallel()
706 trimmedSpec, err := k8s.ConstructTrimmedSpecWithManagedFields(tc.resource, schema, tc.hierarchicalRefs)
707 if err != nil {
708 t.Fatalf("unexpected error: %v", err)
709 }
710 if got, want := trimmedSpec, tc.expected; !test.Equals(t, got, want) {
711 t.Fatalf("got: %v, want: %v", got, want)
712 }
713 })
714 }
715 }
716
717 func fieldPathSetToJSON(t *testing.T, s *fieldpath.Set) []byte {
718 b, err := s.ToJSON()
719 if err != nil {
720 t.Fatal("error converting set to JSON:", err)
721 }
722 return b
723 }
724
725 func mapToManagedFieldEntry(t *testing.T, manager string, fields map[string]interface{}) v1.ManagedFieldsEntry {
726 b, err := json.Marshal(fields)
727 if err != nil {
728 t.Fatal("error marshaling to JSON:", err)
729 }
730 return v1.ManagedFieldsEntry{
731 Manager: manager,
732 FieldsType: k8s.ManagedFieldsTypeFieldsV1,
733 FieldsV1: &v1.FieldsV1{Raw: b},
734 }
735 }
736
View as plain text