1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package krmtotf_test
16
17 import (
18 "reflect"
19 "testing"
20
21 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
22 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp"
23 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
24 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
25 testcontroller "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/controller"
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 corev1 "k8s.io/api/core/v1"
30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31 k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
32 )
33
34 func TestResource_GetImportID(t *testing.T) {
35 tests := []struct {
36 name string
37 rc *v1alpha1.ResourceConfig
38 metadataName string
39 spec map[string]interface{}
40 referencedResources []*unstructured.Unstructured
41 expected string
42 assertGotExpectedErr func(t *testing.T, err error)
43 }{
44 {
45 name: "with fields from spec and status",
46 rc: &v1alpha1.ResourceConfig{
47 IDTemplate: "{{spec_field}}/{{status_field}}",
48 },
49 expected: "abc123/foobar",
50 },
51 {
52 name: "with field from container",
53 rc: &v1alpha1.ResourceConfig{
54 IDTemplate: "project/{{project}}",
55 Containers: []v1alpha1.Container{
56 {
57 Type: v1alpha1.ContainerTypeProject,
58 TFField: "project",
59 },
60 },
61 },
62 expected: "project/my-project-1",
63 },
64 {
65 name: "with reference's ID",
66 rc: &v1alpha1.ResourceConfig{
67 IDTemplate: "bar/{{bar_ref}}",
68 ResourceReferences: []v1alpha1.ReferenceConfig{
69 {
70 TFField: "bar_ref",
71 TypeConfig: v1alpha1.TypeConfig{
72 Key: "barRef",
73 GVK: k8sschema.GroupVersionKind{
74 Group: "test1.cnrm.cloud.google.com",
75 Version: "v1alpha1",
76 Kind: "Test1Bar",
77 },
78 },
79 },
80 },
81 },
82 spec: map[string]interface{}{
83 "barRef": map[string]interface{}{
84 "name": "my-ref1",
85 },
86 },
87 referencedResources: []*unstructured.Unstructured{
88 test.NewBarUnstructured("my-ref1", "", corev1.ConditionTrue),
89 },
90 expected: "bar/my-ref1",
91 },
92 {
93 name: "with reference's ID, but referenced resource is not found",
94 rc: &v1alpha1.ResourceConfig{
95 IDTemplate: "bar/{{bar_ref}}",
96 ResourceReferences: []v1alpha1.ReferenceConfig{
97 {
98 TFField: "bar_ref",
99 TypeConfig: v1alpha1.TypeConfig{
100 Key: "barRef",
101 GVK: k8sschema.GroupVersionKind{
102 Group: "test1.cnrm.cloud.google.com",
103 Version: "v1alpha1",
104 Kind: "Test1Bar",
105 },
106 },
107 },
108 },
109 },
110 spec: map[string]interface{}{
111 "barRef": map[string]interface{}{
112 "name": "my-ref1",
113 },
114 },
115 assertGotExpectedErr: assertIsReferenceNotFoundError,
116 },
117 {
118 name: "with reference's ID, but referenced resource is not ready",
119 rc: &v1alpha1.ResourceConfig{
120 IDTemplate: "bar/{{bar_ref}}",
121 ResourceReferences: []v1alpha1.ReferenceConfig{
122 {
123 TFField: "bar_ref",
124 TypeConfig: v1alpha1.TypeConfig{
125 Key: "barRef",
126 GVK: k8sschema.GroupVersionKind{
127 Group: "test1.cnrm.cloud.google.com",
128 Version: "v1alpha1",
129 Kind: "Test1Bar",
130 },
131 },
132 },
133 },
134 },
135 spec: map[string]interface{}{
136 "barRef": map[string]interface{}{
137 "name": "my-ref1",
138 },
139 },
140 referencedResources: []*unstructured.Unstructured{
141 test.NewBarUnstructured("my-ref1", "", corev1.ConditionFalse),
142 },
143 assertGotExpectedErr: assertIsReferenceNotReadyError,
144 },
145 {
146 name: "with reference's status field",
147 rc: &v1alpha1.ResourceConfig{
148 IDTemplate: "bar/{{bar_ref}}",
149 ResourceReferences: []v1alpha1.ReferenceConfig{
150 {
151 TFField: "bar_ref",
152 TypeConfig: v1alpha1.TypeConfig{
153 Key: "barRef",
154 GVK: k8sschema.GroupVersionKind{
155 Group: "test1.cnrm.cloud.google.com",
156 Version: "v1alpha1",
157 Kind: "Test1Bar",
158 },
159 TargetField: "status_field",
160 },
161 },
162 },
163 },
164 spec: map[string]interface{}{
165 "barRef": map[string]interface{}{
166 "name": "my-ref1",
167 },
168 },
169 referencedResources: []*unstructured.Unstructured{
170 test.NewBarUnstructured("my-ref1", "", corev1.ConditionTrue),
171 },
172 expected: "bar/foobar",
173 },
174 {
175 name: "with reference's status field, but referenced resource is not found",
176 rc: &v1alpha1.ResourceConfig{
177 IDTemplate: "bar/{{bar_ref}}",
178 ResourceReferences: []v1alpha1.ReferenceConfig{
179 {
180 TFField: "bar_ref",
181 TypeConfig: v1alpha1.TypeConfig{
182 Key: "barRef",
183 GVK: k8sschema.GroupVersionKind{
184 Group: "test1.cnrm.cloud.google.com",
185 Version: "v1alpha1",
186 Kind: "Test1Bar",
187 },
188 TargetField: "status_field",
189 },
190 },
191 },
192 },
193 spec: map[string]interface{}{
194 "barRef": map[string]interface{}{
195 "name": "my-ref1",
196 },
197 },
198 assertGotExpectedErr: assertIsReferenceNotFoundError,
199 },
200 {
201 name: "with reference's status field, but referenced resource is not ready",
202 rc: &v1alpha1.ResourceConfig{
203 IDTemplate: "bar/{{bar_ref}}",
204 ResourceReferences: []v1alpha1.ReferenceConfig{
205 {
206 TFField: "bar_ref",
207 TypeConfig: v1alpha1.TypeConfig{
208 Key: "barRef",
209 GVK: k8sschema.GroupVersionKind{
210 Group: "test1.cnrm.cloud.google.com",
211 Version: "v1alpha1",
212 Kind: "Test1Bar",
213 },
214 TargetField: "status_field",
215 },
216 },
217 },
218 },
219 spec: map[string]interface{}{
220 "barRef": map[string]interface{}{
221 "name": "my-ref1",
222 },
223 },
224 referencedResources: []*unstructured.Unstructured{
225 test.NewBarUnstructured("my-ref1", "", corev1.ConditionFalse),
226 },
227 assertGotExpectedErr: assertIsReferenceNotReadyError,
228 },
229 {
230 name: "with reference's spec field",
231 rc: &v1alpha1.ResourceConfig{
232 IDTemplate: "bar/{{bar_ref}}",
233 ResourceReferences: []v1alpha1.ReferenceConfig{
234 {
235 TFField: "bar_ref",
236 TypeConfig: v1alpha1.TypeConfig{
237 Key: "barRef",
238 GVK: k8sschema.GroupVersionKind{
239 Group: "test1.cnrm.cloud.google.com",
240 Version: "v1alpha1",
241 Kind: "Test1Bar",
242 },
243 TargetField: "spec_field",
244 },
245 },
246 },
247 },
248 spec: map[string]interface{}{
249 "barRef": map[string]interface{}{
250 "name": "my-ref1",
251 },
252 },
253 referencedResources: []*unstructured.Unstructured{
254 test.NewBarUnstructured("my-ref1", "", corev1.ConditionTrue),
255 },
256 expected: "bar/abc123",
257 },
258 {
259 name: "with reference's spec field, but referenced resource is not found",
260 rc: &v1alpha1.ResourceConfig{
261 IDTemplate: "bar/{{bar_ref}}",
262 ResourceReferences: []v1alpha1.ReferenceConfig{
263 {
264 TFField: "bar_ref",
265 TypeConfig: v1alpha1.TypeConfig{
266 Key: "barRef",
267 GVK: k8sschema.GroupVersionKind{
268 Group: "test1.cnrm.cloud.google.com",
269 Version: "v1alpha1",
270 Kind: "Test1Bar",
271 },
272 TargetField: "spec_field",
273 },
274 },
275 },
276 },
277 spec: map[string]interface{}{
278 "barRef": map[string]interface{}{
279 "name": "my-ref1",
280 },
281 },
282 assertGotExpectedErr: assertIsReferenceNotFoundError,
283 },
284 {
285 name: "with reference's spec field, but referenced resource is not ready",
286 rc: &v1alpha1.ResourceConfig{
287 IDTemplate: "bar/{{bar_ref}}",
288 ResourceReferences: []v1alpha1.ReferenceConfig{
289 {
290 TFField: "bar_ref",
291 TypeConfig: v1alpha1.TypeConfig{
292 Key: "barRef",
293 GVK: k8sschema.GroupVersionKind{
294 Group: "test1.cnrm.cloud.google.com",
295 Version: "v1alpha1",
296 Kind: "Test1Bar",
297 },
298 TargetField: "spec_field",
299 },
300 },
301 },
302 },
303 spec: map[string]interface{}{
304 "barRef": map[string]interface{}{
305 "name": "my-ref1",
306 },
307 },
308 referencedResources: []*unstructured.Unstructured{
309 test.NewBarUnstructured("my-ref1", "", corev1.ConditionFalse),
310 },
311 assertGotExpectedErr: assertIsReferenceNotReadyError,
312 },
313 {
314 name: "server-generated ID from status",
315 rc: &v1alpha1.ResourceConfig{
316 ServerGeneratedIDField: "status_field",
317 },
318 expected: "foobar",
319 },
320 {
321 name: "server-generated ID from status, but server-generated ID is not found",
322 rc: &v1alpha1.ResourceConfig{
323 ServerGeneratedIDField: "non_existent_status_field",
324 },
325 assertGotExpectedErr: assertIsServerGeneratedIDNotFoundError,
326 },
327 {
328 name: "server-generated ID from status with template",
329 rc: &v1alpha1.ResourceConfig{
330 IDTemplate: "id/{{status_field}}",
331 ServerGeneratedIDField: "status_field",
332 },
333 expected: "id/foobar",
334 },
335 {
336 name: "server-generated ID from status with template, but server-generated ID is not found",
337 rc: &v1alpha1.ResourceConfig{
338 IDTemplate: "id/{{non_existent_status_field}}",
339 ServerGeneratedIDField: "non_existent_status_field",
340 },
341 assertGotExpectedErr: assertIsServerGeneratedIDNotFoundError,
342 },
343 {
344 name: "no template implies {{project}}/{{resource_id}}",
345 rc: &v1alpha1.ResourceConfig{
346 MetadataMapping: v1alpha1.MetadataMapping{
347 Name: "resource_id",
348 },
349 Containers: []v1alpha1.Container{
350 {
351 Type: v1alpha1.ContainerTypeProject,
352 TFField: "project",
353 },
354 },
355 },
356 metadataName: "my-resource",
357 expected: "my-project-1/my-resource",
358 },
359 {
360 name: "regional resources map location to region field",
361 rc: &v1alpha1.ResourceConfig{
362 Locationality: gcp.Regional,
363 IDTemplate: "regions/{{region}}",
364 },
365 expected: "regions/test-location",
366 },
367 {
368 name: "zonal resources map location to zone field",
369 rc: &v1alpha1.ResourceConfig{
370 Locationality: gcp.Zonal,
371 IDTemplate: "zones/{{zone}}",
372 },
373 expected: "zones/test-location",
374 },
375 {
376 name: "optional spec field should resolve when specified",
377 rc: &v1alpha1.ResourceConfig{
378 IDTemplate: "id/{{barField?}}",
379 },
380 spec: map[string]interface{}{
381 "barField": "bar-value",
382 },
383 expected: "id/bar-value",
384 },
385 {
386 name: "optional spec field should not resolve when not specified",
387 rc: &v1alpha1.ResourceConfig{
388 IDTemplate: "id/{{barField?}}/another-field",
389 },
390 spec: map[string]interface{}{},
391 expected: "id//another-field",
392 },
393 {
394 name: "'or' should pick first value when second is not present",
395 rc: &v1alpha1.ResourceConfig{
396 IDTemplate: "id/[text-with-{{field1}}|{{field2}}]/another-string",
397 },
398 spec: map[string]interface{}{
399 "field1": "field1-value",
400 },
401 expected: "id/text-with-field1-value/another-string",
402 },
403 {
404 name: "'or' should pick second value when first is not present",
405 rc: &v1alpha1.ResourceConfig{
406 IDTemplate: "id/[text-with-{{field1}}|{{field2}}]/another-string",
407 },
408 spec: map[string]interface{}{
409 "field2": "field2-value",
410 },
411 expected: "id/field2-value/another-string",
412 },
413 {
414 name: "'or' should pick first value when first and second are present",
415 rc: &v1alpha1.ResourceConfig{
416 IDTemplate: "id/[text-with-{{field1}}|{{field2}}]/another-string",
417 },
418 spec: map[string]interface{}{
419 "field1": "field1-value",
420 "field2": "field2-value",
421 },
422 expected: "id/text-with-field1-value/another-string",
423 },
424 {
425 name: "server-generated resourceID supported",
426 rc: &v1alpha1.ResourceConfig{
427 ServerGeneratedIDField: "resource_id_field_in_status",
428 ResourceID: v1alpha1.ResourceID{
429 TargetField: "resource_id_field_in_status",
430 },
431 },
432 spec: map[string]interface{}{
433 "resourceID": "id-in-spec",
434 },
435 expected: "id-in-spec",
436 },
437 {
438 name: "server-generated resourceID supported, with a value template",
439 rc: &v1alpha1.ResourceConfig{
440 ServerGeneratedIDField: "resource_id_field_in_status",
441 ResourceID: v1alpha1.ResourceID{
442 TargetField: "resource_id_field_in_status",
443 ValueTemplate: "values/{{value}}",
444 },
445 },
446 spec: map[string]interface{}{
447 "resourceID": "id-in-spec",
448 },
449 expected: "values/id-in-spec",
450 },
451 {
452 name: "server-generated resourceID supported, with an ID template",
453 rc: &v1alpha1.ResourceConfig{
454 IDTemplate: "id/{{resource_id_field_in_status}}",
455 ServerGeneratedIDField: "resource_id_field_in_status",
456 ResourceID: v1alpha1.ResourceID{
457 TargetField: "resource_id_field_in_status",
458 },
459 },
460 spec: map[string]interface{}{
461 "resourceID": "id-in-spec",
462 },
463 expected: "id/id-in-spec",
464 },
465 {
466 name: "server-generated resourceID supported, with a value " +
467 "template and an ID template",
468 rc: &v1alpha1.ResourceConfig{
469 IDTemplate: "parents/parent/{{resource_id_field_in_status}}",
470 ServerGeneratedIDField: "resource_id_field_in_status",
471 ResourceID: v1alpha1.ResourceID{
472 TargetField: "resource_id_field_in_status",
473 ValueTemplate: "groups/group/id/{{value}}",
474 },
475 },
476 spec: map[string]interface{}{
477 "resourceID": "id-in-spec",
478 },
479 expected: "parents/parent/groups/group/id/id-in-spec",
480 },
481 {
482 name: "server-generated resourceID supported, with a value " +
483 "template that contains the container",
484 rc: &v1alpha1.ResourceConfig{
485 ServerGeneratedIDField: "resource_id_field_in_status",
486 ResourceID: v1alpha1.ResourceID{
487 TargetField: "resource_id_field_in_status",
488 ValueTemplate: "projects/{{project}}/id/{{value}}",
489 },
490 Containers: []v1alpha1.Container{
491 {
492 Type: v1alpha1.ContainerTypeProject,
493 TFField: "project",
494 },
495 },
496 },
497 spec: map[string]interface{}{
498 "resourceID": "id-in-spec",
499 },
500 expected: "projects/my-project-1/id/id-in-spec",
501 },
502 {
503 name: "server-generated resourceID supported, with a value " +
504 "template that contains a spec field",
505 rc: &v1alpha1.ResourceConfig{
506 ServerGeneratedIDField: "resource_id_field_in_status",
507 ResourceID: v1alpha1.ResourceID{
508 TargetField: "resource_id_field_in_status",
509 ValueTemplate: "groups/{{spec_field}}/id/{{value}}",
510 },
511 },
512 spec: map[string]interface{}{
513 "resourceID": "id-in-spec",
514 "specField": "specTestValue",
515 },
516 expected: "groups/specTestValue/id/id-in-spec",
517 },
518 {
519 name: "server-generated resourceID supported, with a value " +
520 "template that contains a status field",
521 rc: &v1alpha1.ResourceConfig{
522 ServerGeneratedIDField: "resource_id_field_in_status",
523 ResourceID: v1alpha1.ResourceID{
524 TargetField: "resource_id_field_in_status",
525 ValueTemplate: "groups/{{status_field}}/id/{{value}}",
526 },
527 },
528 spec: map[string]interface{}{
529 "resourceID": "id-in-spec",
530 },
531 expected: "groups/foobar/id/id-in-spec",
532 },
533 {
534 name: "server-generated resourceID supported, with a value " +
535 "template containing a reference field",
536 rc: &v1alpha1.ResourceConfig{
537 ServerGeneratedIDField: "resource_id_field_in_status",
538 ResourceID: v1alpha1.ResourceID{
539 TargetField: "resource_id_field_in_status",
540 ValueTemplate: "bar/{{bar_name}}/id/{{value}}",
541 },
542 ResourceReferences: []v1alpha1.ReferenceConfig{
543 {
544 TFField: "bar_name",
545 TypeConfig: v1alpha1.TypeConfig{
546 Key: "barRef",
547 GVK: k8sschema.GroupVersionKind{
548 Group: "test1.cnrm.cloud.google.com",
549 Version: "v1alpha1",
550 Kind: "Test1Bar",
551 },
552 TargetField: "spec_field",
553 Parent: true,
554 },
555 },
556 },
557 },
558 spec: map[string]interface{}{
559 "resourceID": "id-in-spec",
560 "barRef": map[string]interface{}{
561 "name": "my-bar-ref",
562 },
563 },
564 referencedResources: []*unstructured.Unstructured{
565 test.NewBarUnstructured("my-bar-ref", "", corev1.ConditionTrue),
566 },
567 expected: "bar/abc123/id/id-in-spec",
568 },
569 {
570 name: "server-generated resourceID supported, with spec.resourceID " +
571 "unspecified but serverGeneratedIDField in status specified",
572 rc: &v1alpha1.ResourceConfig{
573 ServerGeneratedIDField: "status_field",
574 ResourceID: v1alpha1.ResourceID{
575 TargetField: "status_field",
576 },
577 },
578 expected: "foobar",
579 },
580 {
581 name: "server-generated resourceID supported, with both " +
582 "spec.resourceID and serverGeneratedIDField in status unspecified",
583 rc: &v1alpha1.ResourceConfig{
584 ServerGeneratedIDField: "non_existent_resource_id",
585 ResourceID: v1alpha1.ResourceID{
586 TargetField: "non_existent_resource_id",
587 },
588 },
589 assertGotExpectedErr: assertIsServerGeneratedIDNotFoundError,
590 },
591 {
592 name: "user-specified resourceID supported",
593 rc: &v1alpha1.ResourceConfig{
594 IDTemplate: "id/{{resource_id}}",
595 MetadataMapping: v1alpha1.MetadataMapping{
596 Name: "resource_id",
597 },
598 ResourceID: v1alpha1.ResourceID{
599 TargetField: "resource_id",
600 },
601 },
602 spec: map[string]interface{}{
603 "resourceID": "user-specified-id",
604 },
605 expected: "id/user-specified-id",
606 },
607 {
608 name: "user-specified resourceID supported, with spec.resourceID " +
609 "unspecified but metadata.name specified",
610 rc: &v1alpha1.ResourceConfig{
611 IDTemplate: "{{resource_id}}",
612 MetadataMapping: v1alpha1.MetadataMapping{
613 Name: "resource_id",
614 },
615 ResourceID: v1alpha1.ResourceID{
616 TargetField: "resource_id",
617 },
618 },
619 metadataName: "default-id",
620 expected: "default-id",
621 },
622 {
623 name: "user-specified resourceID supported, with both " +
624 "spec.resourceID and metadata.name unspecified",
625 rc: &v1alpha1.ResourceConfig{
626 IDTemplate: "id/{{resource_id}}",
627 MetadataMapping: v1alpha1.MetadataMapping{
628 Name: "resource_id",
629 },
630 ResourceID: v1alpha1.ResourceID{
631 TargetField: "resource_id",
632 },
633 },
634 assertGotExpectedErr: hasError,
635 },
636 }
637 smLoader := testservicemappingloader.NewForUnitTest(t)
638 for _, tc := range tests {
639 tc := tc
640 t.Run(tc.name, func(t *testing.T) {
641 t.Parallel()
642 testId := testvariable.NewUniqueId()
643 c := mgr.GetClient()
644 r := resourceSkeleton()
645 r.ResourceConfig = *tc.rc
646 if tc.metadataName != "" {
647 r.SetName(tc.metadataName)
648 }
649 r.SetNamespace(testId)
650 testcontroller.EnsureNamespaceExistsT(t, c, testId)
651 bar := test.NewBarUnstructured("my-resource", testId, corev1.ConditionTrue)
652 r.SetAnnotations(bar.GetAnnotations())
653 r.Spec = tc.spec
654 if r.Spec == nil {
655 r.Spec = bar.Object["spec"].(map[string]interface{})
656 }
657 r.Status = bar.Object["status"].(map[string]interface{})
658 for _, obj := range tc.referencedResources {
659 obj.SetNamespace(testId)
660 }
661 test.EnsureObjectsExist(t, tc.referencedResources, c)
662 actual, err := r.GetImportID(c, smLoader)
663 if tc.assertGotExpectedErr != nil {
664 tc.assertGotExpectedErr(t, err)
665 } else if err != nil {
666 t.Fatalf("error getting import ID: %v", err)
667 }
668 if tc.expected != actual {
669 t.Fatalf("expected: %v, actual %v", tc.expected, actual)
670 }
671 })
672 }
673 }
674
675 func assertIsReferenceNotFoundError(t *testing.T, err error) {
676 if _, ok := k8s.AsReferenceNotFoundError(err); !ok {
677 t.Fatalf("expected error that can be unwrapped as '%v', but got: '%v'", reflect.TypeOf(&k8s.ReferenceNotFoundError{}), err)
678 }
679 }
680
681 func assertIsReferenceNotReadyError(t *testing.T, err error) {
682 if _, ok := k8s.AsReferenceNotReadyError(err); !ok {
683 t.Fatalf("expected error that can be unwrapped as '%v', but got: '%v'", reflect.TypeOf(&k8s.ReferenceNotReadyError{}), err)
684 }
685 }
686
687 func assertIsServerGeneratedIDNotFoundError(t *testing.T, err error) {
688 if _, ok := k8s.AsServerGeneratedIDNotFoundError(err); !ok {
689 t.Fatalf("expected error that can be unwrapped as '%v', but got: %v", reflect.TypeOf(&k8s.ServerGeneratedIDNotFoundError{}), err)
690 }
691 }
692
693 func hasError(t *testing.T, err error) {
694 if err == nil {
695 t.Fatalf("got nil, want an error")
696 }
697 }
698
699 func TestResource_ValidateResourceIDIfSupported(t *testing.T) {
700 tests := []struct {
701 name string
702 rc *v1alpha1.ResourceConfig
703 spec map[string]interface{}
704 hasError bool
705 }{
706 {
707 name: "resourceID not supported",
708 rc: &v1alpha1.ResourceConfig{},
709 },
710 {
711 name: "unspecified resourceID",
712 rc: &v1alpha1.ResourceConfig{
713 ResourceID: v1alpha1.ResourceID{
714 TargetField: "test_field",
715 },
716 MetadataMapping: v1alpha1.MetadataMapping{
717 Name: "test_field",
718 },
719 },
720 spec: map[string]interface{}{},
721 },
722 {
723 name: "empty resourceID",
724 rc: &v1alpha1.ResourceConfig{
725 ResourceID: v1alpha1.ResourceID{
726 TargetField: "test_field",
727 },
728 MetadataMapping: v1alpha1.MetadataMapping{
729 Name: "test_field",
730 },
731 },
732 spec: map[string]interface{}{
733 "resourceID": "",
734 },
735 hasError: true,
736 },
737 {
738 name: "nonempty resourceID",
739 rc: &v1alpha1.ResourceConfig{
740 ResourceID: v1alpha1.ResourceID{
741 TargetField: "test_field",
742 },
743 MetadataMapping: v1alpha1.MetadataMapping{
744 Name: "test_field",
745 },
746 },
747 spec: map[string]interface{}{
748 "resourceID": "test-resource-id",
749 },
750 },
751 }
752
753 for _, tc := range tests {
754 tc := tc
755 t.Run(tc.name, func(t *testing.T) {
756 t.Parallel()
757 r := resourceSkeleton()
758 r.ResourceConfig = *tc.rc
759 r.SetName("my-resource")
760 r.Spec = tc.spec
761 if r.Spec == nil {
762 r.Spec = map[string]interface{}{}
763 }
764
765 err := r.ValidateResourceIDIfSupported()
766 if tc.hasError {
767 if err == nil {
768 t.Fatalf("got nil, want an error")
769 }
770 return
771 } else if err != nil {
772 t.Fatalf("error validating the resource ID if supported: %v", err)
773 }
774 })
775 }
776 }
777
778 func TestResource_ConstructServerGeneratedIDInStatusFromResourceID(t *testing.T) {
779 tests := []struct {
780 name string
781 rc *v1alpha1.ResourceConfig
782 spec map[string]interface{}
783 status map[string]interface{}
784 expectedSpec map[string]interface{}
785 expectedStatus map[string]interface{}
786 expectedResult string
787 hasError bool
788 }{
789 {
790 name: "both resourceID field and server-generated ID field are" +
791 "unspecified",
792 rc: &v1alpha1.ResourceConfig{
793 ServerGeneratedIDField: "server_generated_id",
794 ResourceID: v1alpha1.ResourceID{
795 TargetField: "server_generated_id",
796 },
797 },
798 spec: map[string]interface{}{},
799 status: map[string]interface{}{},
800 expectedSpec: map[string]interface{}{},
801 expectedStatus: map[string]interface{}{},
802 expectedResult: "",
803 },
804 {
805 name: "resourceID field is unspecified and server-generated ID " +
806 "field is non-empty",
807 rc: &v1alpha1.ResourceConfig{
808 ServerGeneratedIDField: "server_generated_id",
809 ResourceID: v1alpha1.ResourceID{
810 TargetField: "server_generated_id",
811 },
812 },
813 spec: map[string]interface{}{},
814 status: map[string]interface{}{
815 "serverGeneratedId": "server-generated-id",
816 },
817 expectedSpec: map[string]interface{}{},
818 expectedStatus: map[string]interface{}{
819 "serverGeneratedId": "server-generated-id",
820 },
821 expectedResult: "",
822 },
823 {
824 name: "resourceID field is empty",
825 rc: &v1alpha1.ResourceConfig{
826 ServerGeneratedIDField: "server_generated_id",
827 ResourceID: v1alpha1.ResourceID{
828 TargetField: "server_generated_id",
829 },
830 },
831 spec: map[string]interface{}{
832 "resourceID": "",
833 },
834 status: map[string]interface{}{
835 "serverGeneratedId": "server-generated-id",
836 },
837 hasError: true,
838 },
839 {
840 name: "resourceID field and server-generated ID field have " +
841 "different non-empty values",
842 rc: &v1alpha1.ResourceConfig{
843 ServerGeneratedIDField: "server_generated_id",
844 ResourceID: v1alpha1.ResourceID{
845 TargetField: "server_generated_id",
846 },
847 },
848 spec: map[string]interface{}{
849 "resourceID": "non-empty-resource-id",
850 },
851 status: map[string]interface{}{
852 "serverGeneratedId": "non-empty-id-in-status",
853 },
854 expectedSpec: map[string]interface{}{
855 "resourceID": "non-empty-resource-id",
856 },
857 expectedStatus: map[string]interface{}{
858 "serverGeneratedId": "non-empty-id-in-status",
859 },
860 expectedResult: "non-empty-resource-id",
861 },
862 {
863 name: "resourceID field has a non-empty value and " +
864 "server-generated ID field is unspecified",
865 rc: &v1alpha1.ResourceConfig{
866 ServerGeneratedIDField: "server_generated_id",
867 ResourceID: v1alpha1.ResourceID{
868 TargetField: "server_generated_id",
869 },
870 },
871 spec: map[string]interface{}{
872 "resourceID": "non-empty-resource-id",
873 },
874 status: map[string]interface{}{},
875 expectedSpec: map[string]interface{}{
876 "resourceID": "non-empty-resource-id",
877 },
878 expectedStatus: map[string]interface{}{},
879 expectedResult: "non-empty-resource-id",
880 },
881 {
882 name: "resourceID field has a non-empty value and the nested " +
883 "server-generated ID field is empty",
884 rc: &v1alpha1.ResourceConfig{
885 ServerGeneratedIDField: "parent_field.server_generated_id",
886 ResourceID: v1alpha1.ResourceID{
887 TargetField: "parent_field.server_generated_id",
888 },
889 },
890 spec: map[string]interface{}{
891 "resourceID": "non-empty-resource-id",
892 },
893 status: map[string]interface{}{
894 "parentField": map[string]interface{}{
895 "serverGeneratedId": "",
896 },
897 },
898 expectedSpec: map[string]interface{}{
899 "resourceID": "non-empty-resource-id",
900 },
901 expectedStatus: map[string]interface{}{
902 "parentField": map[string]interface{}{
903 "serverGeneratedId": "",
904 },
905 },
906 expectedResult: "non-empty-resource-id",
907 },
908
909 {
910 name: "with a value template, resourceID field has a " +
911 "non-empty value and server-generated ID field is empty",
912 rc: &v1alpha1.ResourceConfig{
913 ServerGeneratedIDField: "server_generated_id",
914 ResourceID: v1alpha1.ResourceID{
915 TargetField: "server_generated_id",
916 ValueTemplate: "id/{{value}}",
917 },
918 },
919 spec: map[string]interface{}{
920 "resourceID": "non-empty-resource-id",
921 },
922 status: map[string]interface{}{
923 "serverGeneratedId": "",
924 },
925 expectedSpec: map[string]interface{}{
926 "resourceID": "non-empty-resource-id",
927 },
928 expectedStatus: map[string]interface{}{
929 "serverGeneratedId": "",
930 },
931 expectedResult: "id/non-empty-resource-id",
932 },
933 {
934 name: "with a complex value template, resourceID field " +
935 "has a non-empty value and server-generated ID field is empty",
936 rc: &v1alpha1.ResourceConfig{
937 ServerGeneratedIDField: "server_generated_id",
938 ResourceID: v1alpha1.ResourceID{
939 TargetField: "server_generated_id",
940 ValueTemplate: "projects/{{project}}/groups/{{group_id}}/values/{{value}}",
941 },
942 Containers: []v1alpha1.Container{
943 {
944 Type: v1alpha1.ContainerTypeProject,
945 TFField: "project",
946 },
947 },
948 },
949 spec: map[string]interface{}{
950 "resourceID": "resource-id",
951 "groupId": "test-group",
952 },
953 status: map[string]interface{}{
954 "serverGeneratedId": "",
955 },
956 expectedSpec: map[string]interface{}{
957 "resourceID": "resource-id",
958 "groupId": "test-group",
959 },
960 expectedStatus: map[string]interface{}{
961 "serverGeneratedId": "",
962 },
963 expectedResult: "projects/my-project-1/groups/test-group/values/resource-id",
964 },
965 {
966 name: "with a value template containing parent field, the resourceID " +
967 "field has a non-empty value and server-generated ID field is empty",
968 rc: &v1alpha1.ResourceConfig{
969 ServerGeneratedIDField: "server_generated_id",
970 ResourceID: v1alpha1.ResourceID{
971 TargetField: "server_generated_id",
972 ValueTemplate: "{{parent}}/values/{{value}}",
973 },
974 ResourceReferences: []v1alpha1.ReferenceConfig{
975 {
976 TFField: "parent",
977 TypeConfig: v1alpha1.TypeConfig{
978 Key: "parentRef",
979 GVK: k8sschema.GroupVersionKind{
980 Group: "datacatalog.cnrm.cloud.google.com",
981 Version: "v1beta1",
982 Kind: "DataCatalogTaxonomy",
983 },
984 TargetField: "name",
985 Parent: true,
986 },
987 },
988 },
989 },
990 spec: map[string]interface{}{
991 "resourceID": "resource-id",
992 "parentRef": map[string]interface{}{
993 "external": "projects/project/locations/us/taxonomies/tid",
994 },
995 },
996 status: map[string]interface{}{
997 "serverGeneratedId": "",
998 },
999 expectedSpec: map[string]interface{}{
1000 "resourceID": "resource-id",
1001 "parentRef": map[string]interface{}{
1002 "external": "projects/project/locations/us/taxonomies/tid",
1003 },
1004 },
1005 expectedStatus: map[string]interface{}{
1006 "serverGeneratedId": "",
1007 },
1008 expectedResult: "projects/project/locations/us/taxonomies/tid/values/resource-id",
1009 },
1010 {
1011 name: "resourceID field has a non-empty value and status is nil",
1012 rc: &v1alpha1.ResourceConfig{
1013 ServerGeneratedIDField: "server_generated_id",
1014 ResourceID: v1alpha1.ResourceID{
1015 TargetField: "server_generated_id",
1016 },
1017 },
1018 spec: map[string]interface{}{
1019 "resourceID": "non-empty-resource-id",
1020 },
1021 status: nil,
1022 expectedSpec: map[string]interface{}{
1023 "resourceID": "non-empty-resource-id",
1024 },
1025 expectedStatus: nil,
1026 expectedResult: "non-empty-resource-id",
1027 },
1028 }
1029
1030 smLoader := testservicemappingloader.NewForUnitTest(t)
1031 for _, tc := range tests {
1032 tc := tc
1033 t.Run(tc.name, func(t *testing.T) {
1034 t.Parallel()
1035 c := mgr.GetClient()
1036 r := resourceSkeleton()
1037 r.ResourceConfig = *tc.rc
1038 r.SetName("test-resource")
1039 bar := test.NewBarUnstructured("test", "", corev1.ConditionTrue)
1040 r.SetAnnotations(bar.GetAnnotations())
1041 r.Spec = tc.spec
1042 r.Status = tc.status
1043
1044 result, err := r.ConstructServerGeneratedIDInStatusFromResourceID(c, smLoader)
1045 if tc.hasError {
1046 if err == nil {
1047 t.Fatalf("got nil, want an error")
1048 }
1049 return
1050 } else if err != nil {
1051 t.Fatalf("error syncing the server-generated ID in "+
1052 "status from resource ID: %v", err)
1053 }
1054 if got, want := result, tc.expectedResult; got != want {
1055 t.Fatalf("got: %v, want: %v", got, want)
1056 }
1057 if got, want := r.Spec, tc.expectedSpec; !test.Equals(t, got, want) {
1058 t.Fatalf("got: %v, want: %v", got, want)
1059 }
1060 if got, want := r.Status, tc.expectedStatus; !test.Equals(t, got, want) {
1061 t.Fatalf("got: %v, want: %v", got, want)
1062 }
1063 })
1064 }
1065 }
1066
1067 func TestResource_GetServerGeneratedID(t *testing.T) {
1068 tests := []struct {
1069 name string
1070 rc *v1alpha1.ResourceConfig
1071 spec map[string]interface{}
1072 status map[string]interface{}
1073 expectedID string
1074 assertGotExpectedErr func(t *testing.T, err error)
1075 }{
1076 {
1077 name: "get server-generated ID",
1078 rc: &v1alpha1.ResourceConfig{
1079 ServerGeneratedIDField: "test_field",
1080 },
1081 status: map[string]interface{}{
1082 "testField": "test-id",
1083 },
1084 expectedID: "test-id",
1085 },
1086 {
1087 name: "get server-generated ID from nested field",
1088 rc: &v1alpha1.ResourceConfig{
1089 ServerGeneratedIDField: "nested_field.test_field",
1090 },
1091 status: map[string]interface{}{
1092 "nestedField": map[string]interface{}{
1093 "testField": "nested-id",
1094 },
1095 },
1096 expectedID: "nested-id",
1097 },
1098 {
1099 name: "server-generated ID not found",
1100 rc: &v1alpha1.ResourceConfig{
1101 ServerGeneratedIDField: "test_field",
1102 },
1103 status: map[string]interface{}{
1104 "otherStatusField": "testValue",
1105 },
1106 expectedID: "",
1107 assertGotExpectedErr: assertIsServerGeneratedIDNotFoundError,
1108 },
1109 {
1110 name: "get server-generated ID from resource ID",
1111 rc: &v1alpha1.ResourceConfig{
1112 ResourceID: v1alpha1.ResourceID{
1113 TargetField: "test_field",
1114 },
1115 ServerGeneratedIDField: "test_field",
1116 },
1117 spec: map[string]interface{}{
1118 "resourceID": "test-id-in-spec",
1119 },
1120 expectedID: "test-id-in-spec",
1121 },
1122 {
1123 name: "server-generated ID not set in spec.resourceID or in status",
1124 rc: &v1alpha1.ResourceConfig{
1125 ResourceID: v1alpha1.ResourceID{
1126 TargetField: "test_field",
1127 },
1128 ServerGeneratedIDField: "test_field",
1129 },
1130 spec: map[string]interface{}{
1131 "otherSpecField": "testValue",
1132 },
1133 status: map[string]interface{}{
1134 "otherStatusField": "testValue",
1135 },
1136 expectedID: "",
1137 assertGotExpectedErr: assertIsServerGeneratedIDNotFoundError,
1138 },
1139 {
1140 name: "server-generated ID with a valueTemplate not set in " +
1141 "spec.resourceID but in status ",
1142 rc: &v1alpha1.ResourceConfig{
1143 ResourceID: v1alpha1.ResourceID{
1144 TargetField: "test_field",
1145 ValueTemplate: "values/{{value}}",
1146 },
1147 ServerGeneratedIDField: "test_field",
1148 },
1149 spec: map[string]interface{}{
1150 "otherSpecField": "testValue",
1151 },
1152 status: map[string]interface{}{
1153 "testField": "values/test-id",
1154 },
1155 expectedID: "test-id",
1156 },
1157 {
1158 name: "empty spec.resourceID",
1159 rc: &v1alpha1.ResourceConfig{
1160 ResourceID: v1alpha1.ResourceID{
1161 TargetField: "test_field",
1162 },
1163 ServerGeneratedIDField: "test_field",
1164 },
1165 spec: map[string]interface{}{
1166 "resourceID": "",
1167 },
1168 status: map[string]interface{}{},
1169 expectedID: "",
1170 assertGotExpectedErr: hasError,
1171 },
1172 {
1173 name: "spec.resourceID unspecified and serverGeneratedIDField in " +
1174 "status empty",
1175 rc: &v1alpha1.ResourceConfig{
1176 ResourceID: v1alpha1.ResourceID{
1177 TargetField: "test_field",
1178 },
1179 ServerGeneratedIDField: "test_field",
1180 },
1181 spec: map[string]interface{}{},
1182 status: map[string]interface{}{
1183 "testField": "",
1184 },
1185 expectedID: "",
1186 assertGotExpectedErr: hasError,
1187 },
1188 {
1189 name: "spec.resourceID unspecified and serverGeneratedIDField in " +
1190 "status not matching value template",
1191 rc: &v1alpha1.ResourceConfig{
1192 ResourceID: v1alpha1.ResourceID{
1193 TargetField: "test_field",
1194 ValueTemplate: "values/{{value}}",
1195 },
1196 ServerGeneratedIDField: "test_field",
1197 },
1198 spec: map[string]interface{}{},
1199 status: map[string]interface{}{
1200 "testField": "test-123",
1201 },
1202 expectedID: "",
1203 assertGotExpectedErr: hasError,
1204 },
1205 {
1206 name: "spec.resourceID unspecified and resource ID extracted from " +
1207 "serverGeneratedIDField in status empty",
1208 rc: &v1alpha1.ResourceConfig{
1209 ResourceID: v1alpha1.ResourceID{
1210 TargetField: "test_field",
1211 ValueTemplate: "values/{{value}}",
1212 },
1213 ServerGeneratedIDField: "test_field",
1214 },
1215 spec: map[string]interface{}{},
1216 status: map[string]interface{}{
1217 "testField": "values/",
1218 },
1219 expectedID: "",
1220 assertGotExpectedErr: hasError,
1221 },
1222 {
1223 name: "spec.resourceID specified but does not map to server-generated ID",
1224 rc: &v1alpha1.ResourceConfig{
1225 MetadataMapping: v1alpha1.MetadataMapping{
1226 Name: "name",
1227 },
1228 ResourceID: v1alpha1.ResourceID{
1229 TargetField: "name",
1230 },
1231 ServerGeneratedIDField: "test_field",
1232 },
1233 spec: map[string]interface{}{
1234 "resourceID": "test-id-in-spec",
1235 },
1236 status: map[string]interface{}{
1237 "testField": "test-id-in-status",
1238 },
1239 expectedID: "test-id-in-status",
1240 },
1241 {
1242 name: "spec.resourceID specified but does not map to server-generated ID, " +
1243 "and serverGeneratedIDField in status not found",
1244 rc: &v1alpha1.ResourceConfig{
1245 MetadataMapping: v1alpha1.MetadataMapping{
1246 Name: "name",
1247 },
1248 ResourceID: v1alpha1.ResourceID{
1249 TargetField: "name",
1250 },
1251 ServerGeneratedIDField: "test_field",
1252 },
1253 spec: map[string]interface{}{
1254 "resourceID": "test-id-in-spec",
1255 },
1256 status: map[string]interface{}{
1257 "otherStatusField": "testValue",
1258 },
1259 expectedID: "",
1260 assertGotExpectedErr: assertIsServerGeneratedIDNotFoundError,
1261 },
1262 {
1263 name: "spec.resourceID specified and supports a value template, " +
1264 "but does not map to server-generated ID",
1265 rc: &v1alpha1.ResourceConfig{
1266 MetadataMapping: v1alpha1.MetadataMapping{
1267 Name: "name",
1268 },
1269 ResourceID: v1alpha1.ResourceID{
1270 TargetField: "name",
1271 ValueTemplate: "values/{{value}}",
1272 },
1273 ServerGeneratedIDField: "test_field",
1274 },
1275 spec: map[string]interface{}{
1276 "resourceID": "test-id-in-spec",
1277 },
1278 status: map[string]interface{}{
1279 "testField": "test-id-in-status",
1280 },
1281 expectedID: "test-id-in-status",
1282 },
1283 }
1284
1285 for _, tc := range tests {
1286 tc := tc
1287 t.Run(tc.name, func(t *testing.T) {
1288 t.Parallel()
1289 r := resourceSkeleton()
1290 r.ResourceConfig = *tc.rc
1291 r.SetName("my-resource")
1292 r.Spec = tc.spec
1293 if r.Spec == nil {
1294 r.Spec = map[string]interface{}{}
1295 }
1296 r.Status = tc.status
1297 if r.Status == nil {
1298 r.Status = map[string]interface{}{}
1299 }
1300
1301 id, err := r.GetServerGeneratedID()
1302 if tc.assertGotExpectedErr != nil {
1303 tc.assertGotExpectedErr(t, err)
1304 } else if err != nil {
1305 t.Fatalf("error getting server-generated ID: %v", err)
1306 }
1307 if got, want := id, tc.expectedID; got != want {
1308 t.Fatalf("got: %v, want: %v", got, want)
1309 }
1310 })
1311 }
1312 }
1313
1314 func TestResource_GetResourceID(t *testing.T) {
1315 tests := []struct {
1316 name string
1317 rc *v1alpha1.ResourceConfig
1318 metadataName string
1319 spec map[string]interface{}
1320 status map[string]interface{}
1321 expectedID string
1322 hasError bool
1323 }{
1324 {
1325 name: "empty resourceID",
1326 rc: &v1alpha1.ResourceConfig{
1327 ResourceID: v1alpha1.ResourceID{
1328 TargetField: "test_field",
1329 },
1330 },
1331 spec: map[string]interface{}{
1332 "resourceID": "",
1333 },
1334 hasError: true,
1335 },
1336 {
1337 name: "non-empty resource ID",
1338 rc: &v1alpha1.ResourceConfig{
1339 ResourceID: v1alpha1.ResourceID{
1340 TargetField: "test_field",
1341 },
1342 },
1343 spec: map[string]interface{}{
1344 "resourceID": "test-id",
1345 },
1346 expectedID: "test-id",
1347 },
1348 {
1349 name: "user-specified ID from metadata.name",
1350 rc: &v1alpha1.ResourceConfig{
1351 ResourceID: v1alpha1.ResourceID{
1352 TargetField: "test_field",
1353 },
1354 MetadataMapping: v1alpha1.MetadataMapping{
1355 Name: "test_field",
1356 },
1357 },
1358 metadataName: "user-specified-id",
1359 spec: map[string]interface{}{},
1360 expectedID: "user-specified-id",
1361 },
1362 {
1363 name: "user-specified ID not found",
1364 rc: &v1alpha1.ResourceConfig{
1365 ResourceID: v1alpha1.ResourceID{
1366 TargetField: "test_field",
1367 },
1368 MetadataMapping: v1alpha1.MetadataMapping{
1369 Name: "test_field",
1370 },
1371 },
1372 spec: map[string]interface{}{},
1373 hasError: true,
1374 },
1375 {
1376 name: "server-generated ID from status",
1377 rc: &v1alpha1.ResourceConfig{
1378 ResourceID: v1alpha1.ResourceID{
1379 TargetField: "test_field",
1380 },
1381 ServerGeneratedIDField: "test_field",
1382 },
1383 spec: map[string]interface{}{},
1384 status: map[string]interface{}{
1385 "testField": "id-in-status",
1386 },
1387 expectedID: "id-in-status",
1388 },
1389 {
1390 name: "server-generated ID with a value template from status",
1391 rc: &v1alpha1.ResourceConfig{
1392 ResourceID: v1alpha1.ResourceID{
1393 TargetField: "test_field",
1394 ValueTemplate: "values/{{value}}",
1395 },
1396 ServerGeneratedIDField: "test_field",
1397 },
1398 spec: map[string]interface{}{},
1399 status: map[string]interface{}{
1400 "testField": "values/id-with-template-in-status",
1401 },
1402 expectedID: "id-with-template-in-status",
1403 },
1404 {
1405 name: "server-generated ID not found",
1406 rc: &v1alpha1.ResourceConfig{
1407 ResourceID: v1alpha1.ResourceID{
1408 TargetField: "test_field",
1409 },
1410 ServerGeneratedIDField: "test_field",
1411 },
1412 spec: map[string]interface{}{},
1413 status: map[string]interface{}{},
1414 hasError: true,
1415 },
1416 {
1417 name: "server-generated ID from status but it doesn't match the " +
1418 "value template",
1419 rc: &v1alpha1.ResourceConfig{
1420 ResourceID: v1alpha1.ResourceID{
1421 TargetField: "test_field",
1422 ValueTemplate: "values/{{value}}",
1423 },
1424 ServerGeneratedIDField: "test_field",
1425 },
1426 spec: map[string]interface{}{},
1427 status: map[string]interface{}{
1428 "testField": "incorrectly-formatted-id",
1429 },
1430 hasError: true,
1431 },
1432 }
1433
1434 for _, tc := range tests {
1435 tc := tc
1436 t.Run(tc.name, func(t *testing.T) {
1437 t.Parallel()
1438 r := resourceSkeleton()
1439 r.ResourceConfig = *tc.rc
1440 if tc.metadataName != "" {
1441 r.SetName(tc.metadataName)
1442 }
1443 r.Spec = tc.spec
1444 if r.Spec == nil {
1445 r.Spec = map[string]interface{}{}
1446 }
1447 r.Status = tc.status
1448 if r.Status == nil {
1449 r.Status = map[string]interface{}{}
1450 }
1451
1452 id, err := r.GetResourceID()
1453 if tc.hasError {
1454 if err == nil {
1455 t.Fatalf("got nil, want an error")
1456 }
1457 return
1458 } else if err != nil {
1459 t.Fatalf("error getting resource ID: %v", err)
1460 }
1461 if got, want := id, tc.expectedID; got != want {
1462 t.Fatalf("got: %v, want: %v", got, want)
1463 }
1464 })
1465 }
1466 }
1467
View as plain text