1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package krmtotf_test
16
17 import (
18 "context"
19 "testing"
20
21 corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
22 . "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
23 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
24 testcontroller "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/controller"
25 testmain "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/main"
26 testvariable "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/resourcefixture/variable"
27 testservicemappingloader "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/servicemappingloader"
28
29 "github.com/google/go-cmp/cmp"
30 tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
31 "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
32 corev1 "k8s.io/api/core/v1"
33 v1 "k8s.io/api/core/v1"
34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
35 k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
36 "k8s.io/apimachinery/pkg/types"
37 "sigs.k8s.io/controller-runtime/pkg/client"
38 "sigs.k8s.io/controller-runtime/pkg/manager"
39 )
40
41 var mgr manager.Manager
42
43 func resourceSkeleton() *Resource {
44 return &Resource{
45 TFInfo: &terraform.InstanceInfo{},
46 TFResource: &tfschema.Resource{
47 Schema: map[string]*tfschema.Schema{
48 "int_key": {
49 Type: tfschema.TypeInt,
50 Optional: true,
51 },
52 "float_key": {
53 Type: tfschema.TypeFloat,
54 Optional: true,
55 },
56 "string_key": {
57 Type: tfschema.TypeString,
58 Optional: true,
59 },
60 "bool_key": {
61 Type: tfschema.TypeBool,
62 Optional: true,
63 },
64 "nonconfigurable_string_key": {
65 Type: tfschema.TypeString,
66 },
67 "directive_key": {
68 Type: tfschema.TypeBool,
69 },
70 "nested_object_key": {
71 Type: tfschema.TypeList,
72 MaxItems: 1,
73 Optional: true,
74 Elem: &tfschema.Resource{
75 Schema: map[string]*tfschema.Schema{
76 "nested_float_key": {
77 Type: tfschema.TypeFloat,
78 Optional: true,
79 },
80 "nested_simple_reference_key": {
81 Type: tfschema.TypeString,
82 Optional: true,
83 },
84 "nested_complex_reference_key": {
85 Type: tfschema.TypeString,
86 Optional: true,
87 },
88 "nested_sensitive_field_key": {
89 Type: tfschema.TypeString,
90 Optional: true,
91 Sensitive: true,
92 },
93 "nested_map_key": {
94 Type: tfschema.TypeMap,
95 Optional: true,
96 },
97 "nested_nonconfigurable_key": {
98 Type: tfschema.TypeString,
99 },
100 },
101 },
102 },
103 "list_of_objects_key": {
104 Type: tfschema.TypeList,
105 Optional: true,
106 Elem: &tfschema.Resource{
107 Schema: map[string]*tfschema.Schema{
108 "nested_int_key": {
109 Type: tfschema.TypeInt,
110 Optional: true,
111 },
112 "reference_nested_in_list_of_objects_key": {
113 Type: tfschema.TypeString,
114 Optional: true,
115 },
116 "sensitive_field_nested_in_list_of_objects_key": {
117 Type: tfschema.TypeString,
118 Optional: true,
119 Sensitive: true,
120 },
121 "nonconfigurable_field_nested_in_list_of_objects_key": {
122 Type: tfschema.TypeString,
123 },
124 },
125 },
126 },
127 "list_of_primitives_key": {
128 Type: tfschema.TypeList,
129 Optional: true,
130 Elem: &tfschema.Schema{
131 Type: tfschema.TypeString,
132 },
133 },
134 "reference_key": {
135 Type: tfschema.TypeString,
136 Optional: true,
137 },
138 "list_of_references_key": {
139 Type: tfschema.TypeList,
140 Optional: true,
141 Elem: &tfschema.Schema{
142 Type: tfschema.TypeString,
143 },
144 },
145 "complex_set_of_references_key": {
146 Type: tfschema.TypeSet,
147 Optional: true,
148 Elem: &tfschema.Schema{
149 Type: tfschema.TypeString,
150 },
151 },
152 "map_key": {
153 Type: tfschema.TypeMap,
154 Optional: true,
155 },
156 "unused_key": {
157 Type: tfschema.TypeSet,
158 Optional: true,
159 },
160 "primitive_set_key": {
161 Type: tfschema.TypeSet,
162 Optional: true,
163 Elem: &tfschema.Schema{
164 Type: tfschema.TypeString,
165 },
166 },
167 "object_set_key": {
168 Type: tfschema.TypeSet,
169 Optional: true,
170 Elem: &tfschema.Resource{
171 Schema: map[string]*tfschema.Schema{
172 "index": {
173 Type: tfschema.TypeInt,
174 Optional: true,
175 },
176 "nested_bool_key": {
177 Type: tfschema.TypeBool,
178 Optional: true,
179 },
180 },
181 },
182 Set: func(val interface{}) int {
183 m := val.(map[string]interface{})
184 return m["index"].(int)
185 },
186 },
187 "parent_key": {
188 Type: tfschema.TypeString,
189 Optional: true,
190 },
191 "sensitive_field_key": {
192 Type: tfschema.TypeString,
193 Optional: true,
194 Sensitive: true,
195 },
196 "complex_reference_key": {
197 Type: tfschema.TypeString,
198 Optional: true,
199 },
200 },
201 },
202 ResourceConfig: corekccv1alpha1.ResourceConfig{
203 Name: "google_foo",
204 Kind: "Foo",
205 ResourceReferences: []corekccv1alpha1.ReferenceConfig{
206 {
207 TFField: "nested_object_key.nested_simple_reference_key",
208 TypeConfig: corekccv1alpha1.TypeConfig{
209 Key: "nestedRef",
210 GVK: k8sschema.GroupVersionKind{
211 Group: "test1.cnrm.cloud.google.com",
212 Version: "v1alpha1",
213 Kind: "Test1Bar",
214 },
215 },
216 },
217 {
218 TFField: "nested_object_key.nested_complex_reference_key",
219 Types: []corekccv1alpha1.TypeConfig{
220 {
221 Key: "barRef",
222 GVK: k8sschema.GroupVersionKind{
223 Group: "test1.cnrm.cloud.google.com",
224 Version: "v1alpha1",
225 Kind: "Test1Bar",
226 },
227 },
228 {
229 Key: "value",
230 JSONSchemaType: "string",
231 },
232 },
233 },
234 {
235 TFField: "list_of_objects_key.reference_nested_in_list_of_objects_key",
236 TypeConfig: corekccv1alpha1.TypeConfig{
237 Key: "nestedInListOfObjectsRef",
238 GVK: k8sschema.GroupVersionKind{
239 Group: "test1.cnrm.cloud.google.com",
240 Version: "v1alpha1",
241 Kind: "Test1Bar",
242 },
243 },
244 },
245 {
246 TFField: "reference_key",
247 TypeConfig: corekccv1alpha1.TypeConfig{
248 Key: "referenceRef",
249 GVK: k8sschema.GroupVersionKind{
250 Group: "test1.cnrm.cloud.google.com",
251 Version: "v1alpha1",
252 Kind: "Test1Bar",
253 },
254 },
255 },
256 {
257 TFField: "list_of_references_key",
258 TypeConfig: corekccv1alpha1.TypeConfig{
259 GVK: k8sschema.GroupVersionKind{
260 Group: "test1.cnrm.cloud.google.com",
261 Version: "v1alpha1",
262 Kind: "Test1Bar",
263 },
264 },
265 },
266 {
267 TFField: "complex_set_of_references_key",
268 Types: []corekccv1alpha1.TypeConfig{
269 {
270 Key: "subKeyRef",
271 GVK: k8sschema.GroupVersionKind{
272 Group: "test1.cnrm.cloud.google.com",
273 Version: "v1alpha1",
274 Kind: "Test1Bar",
275 },
276 },
277 },
278 },
279 {
280 TFField: "complex_reference_key",
281 Types: []corekccv1alpha1.TypeConfig{
282 {
283 Key: "value",
284 JSONSchemaType: "string",
285 },
286 {
287 Key: "referenceRef",
288 GVK: k8sschema.GroupVersionKind{
289 Group: "test1.cnrm.cloud.google.com",
290 Version: "v1alpha1",
291 Kind: "Test1Bar",
292 },
293 },
294 },
295 },
296 },
297 Directives: []string{
298 "directive_key",
299 },
300 },
301 }
302 }
303
304 func TestKRMResourceSpecsToTFConfig(t *testing.T) {
305 tests := []struct {
306 name string
307 prevSpec map[string]interface{}
308 hasResourceReferences bool
309 hasSecretReferences bool
310 expected map[string]interface{}
311 }{
312 {
313 name: "keys as constant",
314 prevSpec: map[string]interface{}{
315 "intKey": "1",
316 "floatKey": "0.5",
317 "stringKey": "StringVal",
318 "boolKey": "true",
319 },
320 expected: map[string]interface{}{
321 "int_key": 1,
322 "float_key": 0.5,
323 "string_key": "StringVal",
324 "bool_key": true,
325 },
326 },
327 {
328 name: "list of primitives key",
329 prevSpec: map[string]interface{}{
330 "listOfPrimitivesKey": []interface{}{
331 "myString1",
332 "myString2",
333 },
334 },
335 expected: map[string]interface{}{
336 "list_of_primitives_key": []interface{}{
337 "myString1",
338 "myString2",
339 },
340 },
341 },
342 {
343 name: "map key",
344 prevSpec: map[string]interface{}{
345 "mapKey": map[string]interface{}{
346 "myMapKey1": "MyMapValue1",
347 "myMapKey2": "MyMapValue2",
348 },
349 },
350 expected: map[string]interface{}{
351 "map_key": map[string]interface{}{
352 "myMapKey1": "MyMapValue1",
353 "myMapKey2": "MyMapValue2",
354 },
355 },
356 },
357 {
358 name: "nested objects key",
359 hasResourceReferences: true,
360 prevSpec: map[string]interface{}{
361 "nestedObjectKey": map[string]interface{}{
362 "nestedFloatKey": "0.5",
363 "nestedRef": map[string]interface{}{
364 "name": "my-ref1",
365 },
366 "nestedMapKey": map[string]interface{}{
367 "name": "val",
368 },
369 "nestedComplexReferenceKey": map[string]interface{}{
370 "value": "foobar",
371 },
372 },
373 },
374 expected: map[string]interface{}{
375 "nested_object_key": []interface{}{
376 map[string]interface{}{
377 "nested_float_key": 0.5,
378 "nested_simple_reference_key": "my-ref1",
379 "nested_complex_reference_key": "foobar",
380 "nested_map_key": map[string]interface{}{
381 "name": "val",
382 },
383 },
384 },
385 },
386 },
387 {
388 name: "list of objects key",
389 hasResourceReferences: true,
390 prevSpec: map[string]interface{}{
391 "listOfObjectsKey": []interface{}{
392 map[string]interface{}{
393 "nestedIntKey": "2",
394 "nestedInListOfObjectsRef": map[string]interface{}{
395 "name": "my-ref1",
396 },
397 },
398 map[string]interface{}{
399 "nestedIntKey": "3",
400 "nestedInListOfObjectsRef": map[string]interface{}{
401 "name": "my-ref2",
402 },
403 },
404 },
405 },
406 expected: map[string]interface{}{
407 "list_of_objects_key": []interface{}{
408 map[string]interface{}{
409 "nested_int_key": 2,
410 "reference_nested_in_list_of_objects_key": "my-ref1",
411 },
412 map[string]interface{}{
413 "nested_int_key": 3,
414 "reference_nested_in_list_of_objects_key": "my-ref2",
415 },
416 },
417 },
418 },
419 {
420 name: "simple reference key",
421 hasResourceReferences: true,
422 prevSpec: map[string]interface{}{
423 "referenceRef": map[string]interface{}{
424 "name": "my-ref1",
425 },
426 },
427 expected: map[string]interface{}{
428 "reference_key": "my-ref1",
429 },
430 },
431 {
432 name: "sensitive field with simple value",
433 prevSpec: map[string]interface{}{
434 "sensitiveFieldKey": map[string]interface{}{
435 "value": "val",
436 },
437 },
438 expected: map[string]interface{}{
439 "sensitive_field_key": "val",
440 },
441 },
442 {
443 name: "sensitive field with value from secret ref",
444 hasSecretReferences: true,
445 prevSpec: map[string]interface{}{
446 "sensitiveFieldKey": map[string]interface{}{
447 "valueFrom": map[string]interface{}{
448 "secretKeyRef": map[string]interface{}{
449 "name": "secret1",
450 "key": "secret-key1",
451 },
452 },
453 },
454 },
455 expected: map[string]interface{}{
456 "sensitive_field_key": "secret-val1",
457 },
458 },
459 {
460 name: "nested sensitive field with simple value",
461 prevSpec: map[string]interface{}{
462 "nestedObjectKey": map[string]interface{}{
463 "nestedSensitiveFieldKey": map[string]interface{}{
464 "value": "val",
465 },
466 },
467 },
468 expected: map[string]interface{}{
469 "nested_object_key": []interface{}{
470 map[string]interface{}{
471 "nested_sensitive_field_key": "val",
472 },
473 },
474 },
475 },
476 {
477 name: "nested sensitive field with value from secret ref",
478 hasSecretReferences: true,
479 prevSpec: map[string]interface{}{
480 "nestedObjectKey": map[string]interface{}{
481 "nestedSensitiveFieldKey": map[string]interface{}{
482 "valueFrom": map[string]interface{}{
483 "secretKeyRef": map[string]interface{}{
484 "name": "secret1",
485 "key": "secret-key1",
486 },
487 },
488 },
489 },
490 },
491 expected: map[string]interface{}{
492 "nested_object_key": []interface{}{
493 map[string]interface{}{
494 "nested_sensitive_field_key": "secret-val1",
495 },
496 },
497 },
498 },
499 {
500 name: "sensitive field nested in list of objects with simple values",
501 prevSpec: map[string]interface{}{
502 "listOfObjectsKey": []interface{}{
503 map[string]interface{}{
504 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
505 "value": "val1",
506 },
507 },
508 map[string]interface{}{
509 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
510 "value": "val2",
511 },
512 },
513 },
514 },
515 expected: map[string]interface{}{
516 "list_of_objects_key": []interface{}{
517 map[string]interface{}{
518 "sensitive_field_nested_in_list_of_objects_key": "val1",
519 },
520 map[string]interface{}{
521 "sensitive_field_nested_in_list_of_objects_key": "val2",
522 },
523 },
524 },
525 },
526 {
527 name: "sensitive field nested in list of objects with values from secret refs",
528 hasSecretReferences: true,
529 prevSpec: map[string]interface{}{
530 "listOfObjectsKey": []interface{}{
531 map[string]interface{}{
532 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
533 "valueFrom": map[string]interface{}{
534 "secretKeyRef": map[string]interface{}{
535 "name": "secret1",
536 "key": "secret-key1",
537 },
538 },
539 },
540 },
541 map[string]interface{}{
542 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
543 "valueFrom": map[string]interface{}{
544 "secretKeyRef": map[string]interface{}{
545 "name": "secret2",
546 "key": "secret-key2",
547 },
548 },
549 },
550 },
551 },
552 },
553 expected: map[string]interface{}{
554 "list_of_objects_key": []interface{}{
555 map[string]interface{}{
556 "sensitive_field_nested_in_list_of_objects_key": "secret-val1",
557 },
558 map[string]interface{}{
559 "sensitive_field_nested_in_list_of_objects_key": "secret-val2",
560 },
561 },
562 },
563 },
564 {
565 name: "drop nonconfigurable fields",
566 prevSpec: map[string]interface{}{
567 "nonconfigurable_string_key": "true",
568 },
569 expected: map[string]interface{}{},
570 },
571 {
572 name: "drop nonconfigurable nested fields",
573 prevSpec: map[string]interface{}{
574 "nested_object_key": []interface{}{
575 map[string]interface{}{
576 "nested_nonconfigurable_key": "value",
577 },
578 },
579 },
580 expected: map[string]interface{}{
581 "nested_object_key": []interface{}{
582 map[string]interface{}{},
583 },
584 },
585 },
586 {
587 name: "drop nonconfigurable field nested in list of objects",
588 prevSpec: map[string]interface{}{
589 "list_of_objects_key": []interface{}{
590 map[string]interface{}{
591 "nonconfigurable_field_nested_in_list_of_objects_key": "value",
592 },
593 },
594 },
595 expected: map[string]interface{}{
596 "list_of_objects_key": []interface{}{
597 map[string]interface{}{},
598 },
599 },
600 },
601 }
602 smLoader := testservicemappingloader.NewForUnitTest(t)
603 for _, tc := range tests {
604 tc := tc
605 t.Run(tc.name, func(t *testing.T) {
606 t.Parallel()
607 testId := testvariable.NewUniqueId()
608 c := mgr.GetClient()
609 testcontroller.EnsureNamespaceExistsT(t, c, testId)
610 r := resourceSkeleton()
611 r.SetNamespace(testId)
612 r.Spec = tc.prevSpec
613 if tc.hasResourceReferences {
614 references := []*unstructured.Unstructured{
615 test.NewBarUnstructured("my-ref1", testId, corev1.ConditionTrue),
616 test.NewBarUnstructured("my-ref2", testId, corev1.ConditionTrue),
617 }
618 test.EnsureObjectsExist(t, references, c)
619 }
620 if tc.hasSecretReferences {
621 secretsData := []map[string]interface{}{
622 {
623 "secret-key1": "secret-val1",
624 },
625 {
626 "secret-key2": "secret-val2",
627 },
628 }
629 secrets := []*unstructured.Unstructured{
630 test.NewSecretUnstructured("secret1", testId, secretsData[0]),
631 test.NewSecretUnstructured("secret2", testId, secretsData[1]),
632 }
633 test.EnsureObjectsExist(t, secrets, c)
634 }
635 actual, _, err := KRMResourceToTFResourceConfig(r, c, smLoader)
636 if err != nil {
637 t.Fatalf("error convert to TF resource config: %v", err)
638 }
639 if !test.Equals(t, tc.expected, actual.Raw) {
640 t.Fatalf("expected: %v, actual %v", tc.expected, actual.Raw)
641 }
642 })
643 }
644 }
645
646 func TestKRMResourceMetadataToTFConfig(t *testing.T) {
647 tests := []struct {
648 name string
649 rc *corekccv1alpha1.ResourceConfig
650 annotation map[string]string
651 metadataName string
652 expectedConfig map[string]interface{}
653 }{
654 {
655 name: "directives",
656 annotation: map[string]string{
657 "cnrm.cloud.google.com/directive-key": "true",
658 },
659 expectedConfig: map[string]interface{}{
660 "directive_key": true,
661 },
662 },
663 {
664 name: "metadata.name",
665 metadataName: "my-name",
666 expectedConfig: map[string]interface{}{
667 "string_key": "my-name",
668 },
669 rc: &corekccv1alpha1.ResourceConfig{
670 MetadataMapping: corekccv1alpha1.MetadataMapping{
671 Name: "string_key",
672 },
673 },
674 },
675 {
676 name: "metadata.name with value templating",
677 metadataName: "my-name",
678 expectedConfig: map[string]interface{}{
679 "string_key": "resources/my-name",
680 },
681 rc: &corekccv1alpha1.ResourceConfig{
682 MetadataMapping: corekccv1alpha1.MetadataMapping{
683 Name: "string_key",
684 NameValueTemplate: "resources/{{value}}",
685 },
686 },
687 },
688 {
689
690
691
692
693
694
695
696 name: "empty metadata.name",
697 expectedConfig: map[string]interface{}{},
698 rc: &corekccv1alpha1.ResourceConfig{
699 MetadataMapping: corekccv1alpha1.MetadataMapping{
700 Name: "string_key",
701 },
702 },
703 },
704 }
705 smLoader := testservicemappingloader.NewForUnitTest(t)
706 for _, tc := range tests {
707 tc := tc
708 t.Run(tc.name, func(t *testing.T) {
709 t.Parallel()
710 testId := testvariable.NewUniqueId()
711 c := mgr.GetClient()
712 r := resourceSkeleton()
713 if tc.rc != nil {
714 r.ResourceConfig = *tc.rc
715 }
716 r.SetName(tc.metadataName)
717 r.SetNamespace(testId)
718 r.SetAnnotations(tc.annotation)
719 actual, _, err := KRMResourceToTFResourceConfig(r, c, smLoader)
720 if err != nil {
721 t.Fatalf("error convert to TF resource config: %v", err)
722 }
723 if !test.Equals(t, tc.expectedConfig, actual.Raw) {
724 t.Fatalf("expected: %v, actual %v", tc.expectedConfig, actual.Raw)
725 }
726 })
727 }
728 }
729
730 func TestKRMResourceToTFResourceHierarchicalReferencesAndContainers(t *testing.T) {
731 tests := []struct {
732 name string
733 rc *corekccv1alpha1.ResourceConfig
734 annotations map[string]string
735 spec map[string]interface{}
736 hasResourceReference bool
737 expectedConfig map[string]interface{}
738 }{
739 {
740 name: "container annotations are mapped to config if hierarchical references are not supported",
741 rc: &corekccv1alpha1.ResourceConfig{
742 Containers: []corekccv1alpha1.Container{
743 {
744 Type: corekccv1alpha1.ContainerTypeProject,
745 TFField: "parent_key",
746 },
747 },
748 },
749 annotations: map[string]string{
750 "cnrm.cloud.google.com/project-id": "project-id-from-annotations",
751 },
752 expectedConfig: map[string]interface{}{
753 "parent_key": "project-id-from-annotations",
754 },
755 },
756 {
757 name: "container annotations support value templating",
758 rc: &corekccv1alpha1.ResourceConfig{
759 Containers: []corekccv1alpha1.Container{
760 {
761 Type: corekccv1alpha1.ContainerTypeProject,
762 TFField: "parent_key",
763 ValueTemplate: "projects/{{value}}",
764 },
765 },
766 },
767 annotations: map[string]string{
768 "cnrm.cloud.google.com/project-id": "project-id-from-annotation",
769 },
770 expectedConfig: map[string]interface{}{
771 "parent_key": "projects/project-id-from-annotation",
772 },
773 },
774 {
775 name: "hierarchical references are mapped to config over container annotations",
776 rc: &corekccv1alpha1.ResourceConfig{
777 Containers: []corekccv1alpha1.Container{
778 {
779 Type: corekccv1alpha1.ContainerTypeProject,
780 TFField: "parent_key",
781 },
782 },
783 HierarchicalReferences: []corekccv1alpha1.HierarchicalReference{
784 {
785 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
786 Key: "projectRef",
787 },
788 },
789 ResourceReferences: []corekccv1alpha1.ReferenceConfig{
790 {
791 TFField: "parent_key",
792 TypeConfig: corekccv1alpha1.TypeConfig{
793 Key: "projectRef",
794 GVK: k8sschema.GroupVersionKind{
795 Group: "test1.cnrm.cloud.google.com",
796 Version: "v1alpha1",
797 Kind: "Test1Bar",
798 },
799 },
800 },
801 },
802 },
803 annotations: map[string]string{
804 "cnrm.cloud.google.com/project-id": "project-id-from-annotations",
805 },
806 spec: map[string]interface{}{
807 "projectRef": map[string]interface{}{
808 "name": "my-ref",
809 },
810 },
811 hasResourceReference: true,
812 expectedConfig: map[string]interface{}{
813 "parent_key": "my-ref",
814 },
815 },
816 {
817 name: "external hierarchical references are mapped to config over container annotations",
818 rc: &corekccv1alpha1.ResourceConfig{
819 Containers: []corekccv1alpha1.Container{
820 {
821 Type: corekccv1alpha1.ContainerTypeProject,
822 TFField: "parent_key",
823 },
824 },
825 HierarchicalReferences: []corekccv1alpha1.HierarchicalReference{
826 {
827 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
828 Key: "projectRef",
829 },
830 },
831 ResourceReferences: []corekccv1alpha1.ReferenceConfig{
832 {
833 TFField: "parent_key",
834 TypeConfig: corekccv1alpha1.TypeConfig{
835 Key: "projectRef",
836 GVK: k8sschema.GroupVersionKind{
837 Group: "test1.cnrm.cloud.google.com",
838 Version: "v1alpha1",
839 Kind: "Test1Bar",
840 },
841 },
842 },
843 },
844 },
845 annotations: map[string]string{
846 "cnrm.cloud.google.com/project-id": "project-id-from-annotations",
847 },
848 spec: map[string]interface{}{
849 "projectRef": map[string]interface{}{
850 "external": "project-id-from-spec",
851 },
852 },
853 expectedConfig: map[string]interface{}{
854 "parent_key": "project-id-from-spec",
855 },
856 },
857 {
858 name: "hierarchical references are mapped to config for resource that only supports hierarchical references",
859 rc: &corekccv1alpha1.ResourceConfig{
860 HierarchicalReferences: []corekccv1alpha1.HierarchicalReference{
861 {
862 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
863 Key: "projectRef",
864 },
865 },
866 ResourceReferences: []corekccv1alpha1.ReferenceConfig{
867 {
868 TFField: "parent_key",
869 TypeConfig: corekccv1alpha1.TypeConfig{
870 Key: "projectRef",
871 GVK: k8sschema.GroupVersionKind{
872 Group: "test1.cnrm.cloud.google.com",
873 Version: "v1alpha1",
874 Kind: "Test1Bar",
875 },
876 },
877 },
878 },
879 },
880 annotations: map[string]string{
881 "cnrm.cloud.google.com/project-id": "project-id-from-annotations",
882 },
883 spec: map[string]interface{}{
884 "projectRef": map[string]interface{}{
885 "name": "my-ref",
886 },
887 },
888 hasResourceReference: true,
889 expectedConfig: map[string]interface{}{
890 "parent_key": "my-ref",
891 },
892 },
893 {
894 name: "external hierarchical references are mapped to config for resource that only supports hierarchical references",
895 rc: &corekccv1alpha1.ResourceConfig{
896 HierarchicalReferences: []corekccv1alpha1.HierarchicalReference{
897 {
898 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
899 Key: "projectRef",
900 },
901 },
902 ResourceReferences: []corekccv1alpha1.ReferenceConfig{
903 {
904 TFField: "parent_key",
905 TypeConfig: corekccv1alpha1.TypeConfig{
906 Key: "projectRef",
907 GVK: k8sschema.GroupVersionKind{
908 Group: "test1.cnrm.cloud.google.com",
909 Version: "v1alpha1",
910 Kind: "Test1Bar",
911 },
912 },
913 },
914 },
915 },
916 annotations: map[string]string{
917 "cnrm.cloud.google.com/project-id": "project-id-from-annotations",
918 },
919 spec: map[string]interface{}{
920 "projectRef": map[string]interface{}{
921 "external": "project-id-from-spec",
922 },
923 },
924 expectedConfig: map[string]interface{}{
925 "parent_key": "project-id-from-spec",
926 },
927 },
928 {
929
930
931
932
933
934
935
936 name: "spec without hierarchical reference does not result in an error",
937 rc: &corekccv1alpha1.ResourceConfig{
938 HierarchicalReferences: []corekccv1alpha1.HierarchicalReference{
939 {
940 Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
941 Key: "projectRef",
942 },
943 },
944 ResourceReferences: []corekccv1alpha1.ReferenceConfig{
945 {
946 TFField: "parent_key",
947 TypeConfig: corekccv1alpha1.TypeConfig{
948 Key: "projectRef",
949 GVK: k8sschema.GroupVersionKind{
950 Group: "test1.cnrm.cloud.google.com",
951 Version: "v1alpha1",
952 Kind: "Test1Bar",
953 },
954 },
955 },
956 },
957 },
958 spec: map[string]interface{}{},
959 expectedConfig: map[string]interface{}{},
960 },
961 }
962
963 smLoader := testservicemappingloader.NewForUnitTest(t)
964 for _, tc := range tests {
965 tc := tc
966 t.Run(tc.name, func(t *testing.T) {
967 t.Parallel()
968 testId := testvariable.NewUniqueId()
969 c := mgr.GetClient()
970 testcontroller.EnsureNamespaceExistsT(t, c, testId)
971 r := resourceSkeleton()
972 if tc.rc != nil {
973 r.ResourceConfig = *tc.rc
974 }
975 r.SetNamespace(testId)
976 r.SetAnnotations(tc.annotations)
977 r.Spec = tc.spec
978 if tc.hasResourceReference {
979 references := []*unstructured.Unstructured{
980 test.NewBarUnstructured("my-ref", testId, corev1.ConditionTrue),
981 }
982 test.EnsureObjectsExist(t, references, c)
983 }
984 actual, _, err := KRMResourceToTFResourceConfig(r, c, smLoader)
985 if err != nil {
986 t.Fatalf("error converting to TF resource config: %v", err)
987 }
988 if !test.Equals(t, tc.expectedConfig, actual.Raw) {
989 diff := cmp.Diff(tc.expectedConfig, actual.Raw)
990 t.Fatalf("actual TF config did not match expected TF config; diff (-want +got):\n%v", diff)
991 }
992 })
993 }
994 }
995
996 func TestKRMResourceToTFResourceConfigSecretVersions(t *testing.T) {
997 tests := []struct {
998 name string
999 spec map[string]interface{}
1000 referencedSecrets []*unstructured.Unstructured
1001 secretVersionsShouldContainSecrets []string
1002 }{
1003 {
1004 name: "multiple Secret references",
1005 spec: map[string]interface{}{
1006 "sensitiveFieldKey": map[string]interface{}{
1007 "valueFrom": map[string]interface{}{
1008 "secretKeyRef": map[string]interface{}{
1009 "name": "secret1",
1010 "key": "secret-key1",
1011 },
1012 },
1013 },
1014 "nestedObjectKey": map[string]interface{}{
1015 "nestedSensitiveFieldKey": map[string]interface{}{
1016 "valueFrom": map[string]interface{}{
1017 "secretKeyRef": map[string]interface{}{
1018 "name": "secret2",
1019 "key": "secret-key2",
1020 },
1021 },
1022 },
1023 },
1024 "listOfObjectsKey": []interface{}{
1025 map[string]interface{}{
1026 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
1027 "valueFrom": map[string]interface{}{
1028 "secretKeyRef": map[string]interface{}{
1029 "name": "secret3",
1030 "key": "secret-key3",
1031 },
1032 },
1033 },
1034 },
1035 map[string]interface{}{
1036 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
1037 "valueFrom": map[string]interface{}{
1038 "secretKeyRef": map[string]interface{}{
1039 "name": "secret4",
1040 "key": "secret-key4",
1041 },
1042 },
1043 },
1044 },
1045 },
1046 },
1047 referencedSecrets: []*unstructured.Unstructured{
1048 test.NewSecretUnstructured("secret1", "", map[string]interface{}{"secret-key1": "secret-val1"}),
1049 test.NewSecretUnstructured("secret2", "", map[string]interface{}{"secret-key2": "secret-val2"}),
1050 test.NewSecretUnstructured("secret3", "", map[string]interface{}{"secret-key3": "secret-val3"}),
1051 test.NewSecretUnstructured("secret4", "", map[string]interface{}{"secret-key4": "secret-val4"}),
1052 },
1053 secretVersionsShouldContainSecrets: []string{
1054 "secret1",
1055 "secret2",
1056 "secret3",
1057 "secret4",
1058 },
1059 },
1060 {
1061 name: "multiple Secret references, but shared Secret",
1062 spec: map[string]interface{}{
1063 "sensitiveFieldKey": map[string]interface{}{
1064 "valueFrom": map[string]interface{}{
1065 "secretKeyRef": map[string]interface{}{
1066 "name": "secret1",
1067 "key": "secret-key1",
1068 },
1069 },
1070 },
1071 "nestedObjectKey": map[string]interface{}{
1072 "nestedSensitiveFieldKey": map[string]interface{}{
1073 "valueFrom": map[string]interface{}{
1074 "secretKeyRef": map[string]interface{}{
1075 "name": "secret1",
1076 "key": "secret-key1",
1077 },
1078 },
1079 },
1080 },
1081 "listOfObjectsKey": []interface{}{
1082 map[string]interface{}{
1083 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
1084 "valueFrom": map[string]interface{}{
1085 "secretKeyRef": map[string]interface{}{
1086 "name": "secret1",
1087 "key": "secret-key1",
1088 },
1089 },
1090 },
1091 },
1092 map[string]interface{}{
1093 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
1094 "valueFrom": map[string]interface{}{
1095 "secretKeyRef": map[string]interface{}{
1096 "name": "secret1",
1097 "key": "secret-key1",
1098 },
1099 },
1100 },
1101 },
1102 },
1103 },
1104 referencedSecrets: []*unstructured.Unstructured{
1105 test.NewSecretUnstructured("secret1", "", map[string]interface{}{"secret-key1": "secret-val1"}),
1106 test.NewSecretUnstructured("secret2", "", map[string]interface{}{"secret-key2": "secret-val2"}),
1107 test.NewSecretUnstructured("secret3", "", map[string]interface{}{"secret-key3": "secret-val3"}),
1108 test.NewSecretUnstructured("secret4", "", map[string]interface{}{"secret-key4": "secret-val4"}),
1109 },
1110 secretVersionsShouldContainSecrets: []string{
1111 "secret1",
1112 },
1113 },
1114 {
1115 name: "no Secret references",
1116 spec: map[string]interface{}{
1117 "sensitiveFieldKey": map[string]interface{}{
1118 "value": "val",
1119 },
1120 "nestedObjectKey": map[string]interface{}{
1121 "nestedSensitiveFieldKey": map[string]interface{}{
1122 "value": "val",
1123 },
1124 },
1125 "listOfObjectsKey": []interface{}{
1126 map[string]interface{}{
1127 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
1128 "value": "val",
1129 },
1130 },
1131 map[string]interface{}{
1132 "sensitiveFieldNestedInListOfObjectsKey": map[string]interface{}{
1133 "value": "val",
1134 },
1135 },
1136 },
1137 },
1138 referencedSecrets: []*unstructured.Unstructured{
1139 test.NewSecretUnstructured("secret1", "", map[string]interface{}{"secret-key1": "secret-val1"}),
1140 test.NewSecretUnstructured("secret2", "", map[string]interface{}{"secret-key2": "secret-val2"}),
1141 test.NewSecretUnstructured("secret3", "", map[string]interface{}{"secret-key3": "secret-val3"}),
1142 test.NewSecretUnstructured("secret4", "", map[string]interface{}{"secret-key4": "secret-val4"}),
1143 },
1144 secretVersionsShouldContainSecrets: []string{},
1145 },
1146 {
1147 name: "no sensitive fields",
1148 spec: map[string]interface{}{
1149 "stringKey": "StringVal",
1150 },
1151 referencedSecrets: []*unstructured.Unstructured{
1152 test.NewSecretUnstructured("secret1", "", map[string]interface{}{"secret-key1": "secret-val1"}),
1153 test.NewSecretUnstructured("secret2", "", map[string]interface{}{"secret-key2": "secret-val2"}),
1154 test.NewSecretUnstructured("secret3", "", map[string]interface{}{"secret-key3": "secret-val3"}),
1155 test.NewSecretUnstructured("secret4", "", map[string]interface{}{"secret-key4": "secret-val4"}),
1156 },
1157 secretVersionsShouldContainSecrets: []string{},
1158 },
1159 }
1160 smLoader := testservicemappingloader.NewForUnitTest(t)
1161 for _, tc := range tests {
1162 tc := tc
1163 t.Run(tc.name, func(t *testing.T) {
1164 t.Parallel()
1165 testId := testvariable.NewUniqueId()
1166 c := mgr.GetClient()
1167 testcontroller.EnsureNamespaceExistsT(t, c, testId)
1168 r := resourceSkeleton()
1169 r.SetNamespace(testId)
1170 r.Spec = tc.spec
1171 for _, obj := range tc.referencedSecrets {
1172 obj.SetNamespace(testId)
1173 }
1174 test.EnsureObjectsExist(t, tc.referencedSecrets, c)
1175 expectedSecretVersions := make(map[string]string)
1176 for _, secretName := range tc.secretVersionsShouldContainSecrets {
1177 version, err := getResourceVersionOfSecret(secretName, testId, c)
1178 if err != nil {
1179 t.Fatalf("error determining version of Secret %v: %v", secretName, err)
1180 }
1181 expectedSecretVersions[secretName] = version
1182 }
1183 _, secretVersions, err := KRMResourceToTFResourceConfig(r, c, smLoader)
1184 if err != nil {
1185 t.Fatalf("error converting to TF resource config: %v", err)
1186 }
1187 if !test.Equals(t, expectedSecretVersions, secretVersions) {
1188 t.Fatalf("got: %v, wanted: %v", secretVersions, expectedSecretVersions)
1189 }
1190 })
1191 }
1192 }
1193
1194 func getResourceVersionOfSecret(name, namespace string, c client.Client) (string, error) {
1195 nn := types.NamespacedName{
1196 Name: name,
1197 Namespace: namespace,
1198 }
1199 secret := v1.Secret{}
1200 if err := c.Get(context.TODO(), nn, &secret); err != nil {
1201 return "", err
1202 }
1203 return secret.GetResourceVersion(), nil
1204 }
1205
1206 func TestKRMResourceResourceIDToTFConfig(t *testing.T) {
1207 tests := []struct {
1208 name string
1209 rc *corekccv1alpha1.ResourceConfig
1210 metadataName string
1211 prevSpec map[string]interface{}
1212 expectedConfig map[string]interface{}
1213 hasError bool
1214 }{
1215 {
1216 name: "nonempty user-specified resource ID",
1217 rc: &corekccv1alpha1.ResourceConfig{
1218 ResourceID: corekccv1alpha1.ResourceID{
1219 TargetField: "string_key",
1220 },
1221 MetadataMapping: corekccv1alpha1.MetadataMapping{
1222 Name: "string_key",
1223 },
1224 },
1225 metadataName: "metadata-name",
1226 prevSpec: map[string]interface{}{
1227 "resourceID": "resource-id",
1228 },
1229 expectedConfig: map[string]interface{}{
1230 "string_key": "resource-id",
1231 },
1232 },
1233 {
1234 name: "nonempty server-generated resource ID",
1235 rc: &corekccv1alpha1.ResourceConfig{
1236 ResourceID: corekccv1alpha1.ResourceID{
1237 TargetField: "string_key",
1238 },
1239 ServerGeneratedIDField: "string_key",
1240 },
1241 prevSpec: map[string]interface{}{
1242 "resourceID": "resource-id",
1243 },
1244 expectedConfig: map[string]interface{}{},
1245 },
1246 {
1247 name: "resource ID with value template",
1248 rc: &corekccv1alpha1.ResourceConfig{
1249 ResourceID: corekccv1alpha1.ResourceID{
1250 TargetField: "string_key",
1251 ValueTemplate: "values/{{value}}",
1252 },
1253 MetadataMapping: corekccv1alpha1.MetadataMapping{
1254 Name: "string_key",
1255 NameValueTemplate: "values/{{value}}",
1256 },
1257 },
1258 metadataName: "metadata-name",
1259 prevSpec: map[string]interface{}{
1260 "resourceID": "resource-id",
1261 },
1262 expectedConfig: map[string]interface{}{
1263 "string_key": "values/resource-id",
1264 },
1265 },
1266 {
1267 name: "empty resource ID",
1268 rc: &corekccv1alpha1.ResourceConfig{
1269 ResourceID: corekccv1alpha1.ResourceID{
1270 TargetField: "string_key",
1271 },
1272 MetadataMapping: corekccv1alpha1.MetadataMapping{
1273 Name: "string_key",
1274 },
1275 },
1276 metadataName: "metadata-name",
1277 prevSpec: map[string]interface{}{
1278 "resourceID": "",
1279 },
1280 hasError: true,
1281 },
1282 {
1283 name: "unspecified resource ID with non-empty metadata.name",
1284 rc: &corekccv1alpha1.ResourceConfig{
1285 ResourceID: corekccv1alpha1.ResourceID{
1286 TargetField: "string_key",
1287 },
1288 MetadataMapping: corekccv1alpha1.MetadataMapping{
1289 Name: "string_key",
1290 },
1291 },
1292 metadataName: "metadata-name",
1293 prevSpec: map[string]interface{}{},
1294 expectedConfig: map[string]interface{}{
1295 "string_key": "metadata-name",
1296 },
1297 },
1298 {
1299
1300
1301
1302
1303
1304
1305
1306 name: "unspecified resource ID and unspecified metadata.name",
1307 rc: &corekccv1alpha1.ResourceConfig{
1308 ResourceID: corekccv1alpha1.ResourceID{
1309 TargetField: "string_key",
1310 },
1311 MetadataMapping: corekccv1alpha1.MetadataMapping{
1312 Name: "string_key",
1313 },
1314 },
1315 prevSpec: map[string]interface{}{},
1316 expectedConfig: map[string]interface{}{},
1317 },
1318 }
1319 smLoader := testservicemappingloader.NewForUnitTest(t)
1320 for _, tc := range tests {
1321 tc := tc
1322 t.Run(tc.name, func(t *testing.T) {
1323 t.Parallel()
1324 testId := testvariable.NewUniqueId()
1325 c := mgr.GetClient()
1326 r := resourceSkeleton()
1327 if tc.rc != nil {
1328 r.ResourceConfig = *tc.rc
1329 }
1330 if tc.metadataName != "" {
1331 r.SetName(tc.metadataName)
1332 }
1333 r.SetNamespace(testId)
1334 r.Spec = tc.prevSpec
1335
1336 actual, _, err := KRMResourceToTFResourceConfig(r, c, smLoader)
1337 if tc.hasError {
1338 if err == nil {
1339 t.Fatalf("got nil, want an error")
1340 }
1341 return
1342 } else if err != nil {
1343 t.Fatalf("error converting KRM resource to TF "+
1344 "resource config: %v", err)
1345 }
1346
1347 if got, want := actual.Raw, tc.expectedConfig; !test.Equals(t, got, want) {
1348 t.Fatalf("got: %v, want: %v", got, want)
1349 }
1350 })
1351 }
1352 }
1353
1354 func TestMain(m *testing.M) {
1355 testmain.TestMainForUnitTests(m, &mgr)
1356 }
1357
View as plain text