1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package k8s_test
16
17 import (
18 "fmt"
19 "testing"
20
21 corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
22 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
23 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test"
24 testmain "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test/main"
25 "github.com/appscode/jsonpatch"
26 tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
27 "github.com/nasa9084/go-openapi"
28 corev1 "k8s.io/api/core/v1"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30
31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32 runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
33 "sigs.k8s.io/controller-runtime/pkg/manager"
34 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
35 )
36
37 var (
38 mgr manager.Manager
39 )
40
41 func TestIsDeleted(t *testing.T) {
42 nowTime := metav1.Now()
43 testCases := []struct {
44 Name string
45 Time *metav1.Time
46 ExpectedResult bool
47 }{
48 {"Nil time", nil, false},
49 {"Now time", &nowTime, true},
50 }
51 for _, tc := range testCases {
52 t.Run(tc.Name, func(t *testing.T) {
53 meta := metav1.ObjectMeta{
54 DeletionTimestamp: tc.Time,
55 }
56 result := k8s.IsDeleted(&meta)
57 if result != tc.ExpectedResult {
58 t.Errorf("result mismatch: got '%v', want '%v'", result, tc.ExpectedResult)
59 }
60 })
61 }
62 }
63
64 func TestGVKToGVR(t *testing.T) {
65 tests := []struct {
66 gvk runtimeschema.GroupVersionKind
67 expectedGVR runtimeschema.GroupVersionResource
68 }{
69 {
70 gvk: runtimeschema.GroupVersionKind{Kind: "ComputeVPNGateway"},
71 expectedGVR: runtimeschema.GroupVersionResource{Resource: "computevpngateways"},
72 },
73 {
74 gvk: runtimeschema.GroupVersionKind{Kind: "KMSCryptoKey"},
75 expectedGVR: runtimeschema.GroupVersionResource{Resource: "kmscryptokeys"},
76 },
77 {
78 gvk: runtimeschema.GroupVersionKind{Kind: "IAMPolicy"},
79 expectedGVR: runtimeschema.GroupVersionResource{Resource: "iampolicies"},
80 },
81 {
82 gvk: runtimeschema.GroupVersionKind{Kind: "ComputeAddress"},
83 expectedGVR: runtimeschema.GroupVersionResource{Resource: "computeaddresses"},
84 },
85 {
86 gvk: runtimeschema.GroupVersionKind{Kind: "FirestoreIndex"},
87 expectedGVR: runtimeschema.GroupVersionResource{Resource: "firestoreindexes"},
88 },
89 {
90 gvk: runtimeschema.GroupVersionKind{Kind: "NetworkServicesMesh"},
91 expectedGVR: runtimeschema.GroupVersionResource{Resource: "networkservicesmeshes"},
92 },
93 {
94 gvk: runtimeschema.GroupVersionKind{Kind: "PubSubTopic"},
95 expectedGVR: runtimeschema.GroupVersionResource{Resource: "pubsubtopics"},
96 },
97 }
98 for _, tc := range tests {
99 if got, want := k8s.ToGVR(tc.gvk), tc.expectedGVR; got != want {
100 t.Errorf("result mismatch: got '%v', want '%v'", got, want)
101 }
102 }
103 }
104
105 func TestHasAbandonAnnotation(t *testing.T) {
106 tests := []struct {
107 name string
108 annotations map[string]string
109 hasAbandonAnnotation bool
110 }{
111 {
112 name: "has deletion policy annotation set as abandon",
113 annotations: map[string]string{
114 k8s.DeletionPolicyAnnotation: k8s.DeletionPolicyAbandon,
115 },
116 hasAbandonAnnotation: true,
117 },
118 {
119 name: "has deletion policy annotation set as delete",
120 annotations: map[string]string{
121 k8s.DeletionPolicyAnnotation: k8s.DeletionPolicyDelete,
122 },
123 hasAbandonAnnotation: false,
124 },
125 {
126 name: "has deletion policy annotation set to empty string",
127 annotations: map[string]string{
128 k8s.DeletionPolicyAnnotation: "",
129 },
130 hasAbandonAnnotation: false,
131 },
132 {
133 name: "has no deletion policy annotation",
134 annotations: map[string]string{},
135 hasAbandonAnnotation: false,
136 },
137 {
138 name: "has nil annotations map",
139 hasAbandonAnnotation: false,
140 },
141 }
142 for _, tc := range tests {
143 tc := tc
144 t.Run(tc.name, func(t *testing.T) {
145 t.Parallel()
146 obj := &unstructured.Unstructured{}
147 obj.SetAnnotations(tc.annotations)
148 actual := k8s.HasAbandonAnnotation(obj)
149 if actual != tc.hasAbandonAnnotation {
150 t.Errorf("incorrect value for HasAbandonAnnotation(): got %v, want %v", actual, tc.hasAbandonAnnotation)
151 }
152 })
153 }
154 }
155
156 func TestSetDefaultContainerAnnotation(t *testing.T) {
157 const (
158 nsName = "namespace-1"
159 projectID = "project-1"
160 folderID = "1234567890"
161 orgID = "0987654321"
162 )
163 tests := []struct {
164 name string
165 objAnnotations map[string]string
166 nsAnnotations map[string]string
167 containers []corekccv1alpha1.Container
168 expectedPatches []jsonpatch.JsonPatchOperation
169 shouldErr bool
170 }{
171 {
172 name: "no defaulting if containers list is empty",
173 nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
174 containers: []corekccv1alpha1.Container{},
175 },
176 {
177 name: "prefer resource-level to namespace-level annotation for same type",
178 objAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
179 nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: "other-project-id"},
180 containers: []corekccv1alpha1.Container{
181 {Type: corekccv1alpha1.ContainerTypeProject},
182 },
183 },
184 {
185 name: "prefer resource-level to namespace-level annotation for different types",
186 objAnnotations: map[string]string{k8s.FolderIDAnnotation: folderID},
187 nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
188 containers: []corekccv1alpha1.Container{
189 {Type: corekccv1alpha1.ContainerTypeProject},
190 {Type: corekccv1alpha1.ContainerTypeFolder},
191 },
192 },
193 {
194 name: "prefer resource-level annotation to namespace name",
195 objAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
196 containers: []corekccv1alpha1.Container{
197 {Type: corekccv1alpha1.ContainerTypeProject},
198 },
199 },
200 {
201 name: "add annotation from namespace-level when no resource-level annotation present",
202 objAnnotations: map[string]string{"key": "value"},
203 nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
204 containers: []corekccv1alpha1.Container{
205 {Type: corekccv1alpha1.ContainerTypeProject},
206 },
207 expectedPatches: []jsonpatch.JsonPatchOperation{{
208 Operation: "add",
209 Path: fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "project-id"),
210 Value: projectID,
211 }},
212 },
213 {
214 name: "defaulting creates a new annotations map when none present",
215 nsAnnotations: map[string]string{k8s.ProjectIDAnnotation: projectID},
216 containers: []corekccv1alpha1.Container{
217 {Type: corekccv1alpha1.ContainerTypeProject},
218 },
219 expectedPatches: []jsonpatch.JsonPatchOperation{{
220 Operation: "add",
221 Path: "/metadata/annotations",
222 Value: map[string]interface{}{k8s.ProjectIDAnnotation: projectID},
223 }},
224 },
225 {
226 name: "project-scoped resources use namespace name as project ID when no override present",
227 objAnnotations: map[string]string{"key": "value"},
228 containers: []corekccv1alpha1.Container{
229 {Type: corekccv1alpha1.ContainerTypeProject},
230 },
231 expectedPatches: []jsonpatch.JsonPatchOperation{{
232 Operation: "add",
233 Path: fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "project-id"),
234 Value: nsName,
235 }},
236 },
237 {
238 name: "folder-scoped resources use folder ID annotation",
239 objAnnotations: map[string]string{"key": "value"},
240 nsAnnotations: map[string]string{k8s.FolderIDAnnotation: folderID},
241 containers: []corekccv1alpha1.Container{
242 {Type: corekccv1alpha1.ContainerTypeFolder},
243 },
244 expectedPatches: []jsonpatch.JsonPatchOperation{{
245 Operation: "add",
246 Path: fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "folder-id"),
247 Value: folderID,
248 }},
249 },
250 {
251 name: "org-scoped resources use org ID annotation",
252 objAnnotations: map[string]string{"key": "value"},
253 nsAnnotations: map[string]string{k8s.OrgIDAnnotation: orgID},
254 containers: []corekccv1alpha1.Container{
255 {Type: corekccv1alpha1.ContainerTypeOrganization},
256 },
257 expectedPatches: []jsonpatch.JsonPatchOperation{{
258 Operation: "add",
259 Path: fmt.Sprintf("/metadata/annotations/%v~1%v", k8s.AnnotationPrefix, "organization-id"),
260 Value: orgID,
261 }},
262 },
263 {
264 name: "fail if no default can be determined for non-project-scoped resources",
265 containers: []corekccv1alpha1.Container{
266 {Type: corekccv1alpha1.ContainerTypeOrganization},
267 },
268 shouldErr: true,
269 },
270 {
271 name: "fail if ambiguous resource-level container annotation",
272 objAnnotations: map[string]string{
273 k8s.FolderIDAnnotation: folderID,
274 k8s.OrgIDAnnotation: orgID,
275 },
276 containers: []corekccv1alpha1.Container{
277 {Type: corekccv1alpha1.ContainerTypeFolder},
278 {Type: corekccv1alpha1.ContainerTypeOrganization},
279 },
280 shouldErr: true,
281 },
282 {
283 name: "fail if ambiguous resource-level container annotation (with one being set to empty string)",
284 objAnnotations: map[string]string{
285 k8s.FolderIDAnnotation: "",
286 k8s.OrgIDAnnotation: orgID,
287 },
288 containers: []corekccv1alpha1.Container{
289 {Type: corekccv1alpha1.ContainerTypeFolder},
290 {Type: corekccv1alpha1.ContainerTypeOrganization},
291 },
292 shouldErr: true,
293 },
294 {
295 name: "fail if ambiguous resource-level container annotation (with both being set to empty string)",
296 objAnnotations: map[string]string{
297 k8s.FolderIDAnnotation: "",
298 k8s.OrgIDAnnotation: "",
299 },
300 containers: []corekccv1alpha1.Container{
301 {Type: corekccv1alpha1.ContainerTypeFolder},
302 {Type: corekccv1alpha1.ContainerTypeOrganization},
303 },
304 shouldErr: true,
305 },
306 {
307 name: "fail if ambiguous namespace-level container annotation",
308 nsAnnotations: map[string]string{
309 k8s.FolderIDAnnotation: folderID,
310 k8s.OrgIDAnnotation: orgID,
311 },
312 containers: []corekccv1alpha1.Container{
313 {Type: corekccv1alpha1.ContainerTypeFolder},
314 {Type: corekccv1alpha1.ContainerTypeOrganization},
315 },
316 shouldErr: true,
317 },
318 {
319 name: "fail if ambiguous namespace-level container annotation (with one being set to empty string)",
320 nsAnnotations: map[string]string{
321 k8s.FolderIDAnnotation: "",
322 k8s.OrgIDAnnotation: orgID,
323 },
324 containers: []corekccv1alpha1.Container{
325 {Type: corekccv1alpha1.ContainerTypeFolder},
326 {Type: corekccv1alpha1.ContainerTypeOrganization},
327 },
328 shouldErr: true,
329 },
330 {
331 name: "fail if ambiguous namespace-level container annotation (with both being set to empty string)",
332 nsAnnotations: map[string]string{
333 k8s.FolderIDAnnotation: "",
334 k8s.OrgIDAnnotation: "",
335 },
336 containers: []corekccv1alpha1.Container{
337 {Type: corekccv1alpha1.ContainerTypeFolder},
338 {Type: corekccv1alpha1.ContainerTypeOrganization},
339 },
340 shouldErr: true,
341 },
342 }
343 for _, tc := range tests {
344 tc := tc
345 t.Run(tc.name, func(t *testing.T) {
346 ns := &corev1.Namespace{}
347 ns.SetName(nsName)
348 ns.SetAnnotations(tc.nsAnnotations)
349
350 obj := &unstructured.Unstructured{}
351 obj.SetNamespace(nsName)
352 obj.SetAnnotations(tc.objAnnotations)
353
354 newObj := obj.DeepCopy()
355 err := k8s.SetDefaultContainerAnnotation(newObj, ns, tc.containers)
356 if tc.shouldErr {
357 if err == nil {
358 t.Errorf("expected error but there was none")
359 }
360 return
361 } else {
362 if err != nil {
363 t.Errorf("error setting default container annotation: %v", err)
364 return
365 }
366 }
367 objRaw, err := obj.MarshalJSON()
368 if err != nil {
369 t.Fatalf("error marshaling old object as JSON: %v", err)
370 }
371 newObjRaw, err := newObj.MarshalJSON()
372 if err != nil {
373 t.Fatalf("error marshaling new object as JSON: %v", err)
374 }
375 patches := admission.PatchResponseFromRaw(objRaw, newObjRaw).Patches
376 if len(patches) != len(tc.expectedPatches) {
377 t.Errorf("expected %v patch(es), but got %v; expected: %+v, actual: %+v",
378 len(tc.expectedPatches), len(patches), tc.expectedPatches, patches)
379 return
380 }
381
382 for i, p := range patches {
383 if !test.Equals(t, tc.expectedPatches[i], p) {
384 t.Errorf("expected patch: %+v, actual patch: %+v", tc.expectedPatches[i], p)
385 }
386 }
387 })
388 }
389 }
390
391 func TestValidateOrDefaultManagementConflictPreventionAnnotationForTFBasedResource(t *testing.T) {
392 tests := []struct {
393 Name string
394 ManagementConflictNamespaceAnnotation string
395 ManagementConflictObjectAnnotation string
396 MetadataMappingLabels string
397 LabelsFieldIsMutable bool
398 ExpectedObjectAnnotation string
399 ShouldSucceed bool
400 }{
401 {
402 Name: "none policy on namespace, empty on object",
403 ManagementConflictNamespaceAnnotation: "none",
404 ManagementConflictObjectAnnotation: "",
405 MetadataMappingLabels: "",
406 ExpectedObjectAnnotation: "none",
407 ShouldSucceed: true,
408 },
409 {
410 Name: "none policy on namespace, resource on object",
411 ManagementConflictNamespaceAnnotation: "none",
412 ManagementConflictObjectAnnotation: "resource",
413 MetadataMappingLabels: "labels_field",
414 LabelsFieldIsMutable: true,
415 ExpectedObjectAnnotation: "resource",
416 ShouldSucceed: true,
417 },
418 {
419 Name: "none policy on namespace, none on object",
420 ManagementConflictNamespaceAnnotation: "none",
421 ManagementConflictObjectAnnotation: "none",
422 MetadataMappingLabels: "",
423 ExpectedObjectAnnotation: "none",
424 ShouldSucceed: true,
425 },
426 {
427 Name: "resource policy on namespace, empty on object",
428 ManagementConflictNamespaceAnnotation: "resource",
429 ManagementConflictObjectAnnotation: "",
430 MetadataMappingLabels: "labels_field",
431 LabelsFieldIsMutable: true,
432 ExpectedObjectAnnotation: "resource",
433 ShouldSucceed: true,
434 },
435 {
436 Name: "resource policy on namespace, resource on object",
437 ManagementConflictNamespaceAnnotation: "resource",
438 ManagementConflictObjectAnnotation: "resource",
439 MetadataMappingLabels: "labels_field",
440 LabelsFieldIsMutable: true,
441 ExpectedObjectAnnotation: "resource",
442 ShouldSucceed: true,
443 },
444 {
445 Name: "resource policy on namespace, none on object",
446 ManagementConflictNamespaceAnnotation: "resource",
447 ManagementConflictObjectAnnotation: "none",
448 MetadataMappingLabels: "labels_field",
449 LabelsFieldIsMutable: true,
450 ExpectedObjectAnnotation: "none",
451 ShouldSucceed: true,
452 },
453 {
454 Name: "resource policy on namespace with no labels support should default to none",
455 ManagementConflictNamespaceAnnotation: "resource",
456 ManagementConflictObjectAnnotation: "",
457 MetadataMappingLabels: "",
458 ExpectedObjectAnnotation: "none",
459 ShouldSucceed: true,
460 },
461 {
462 Name: "resource policy on namespace with immutable labels should default to none",
463 ManagementConflictNamespaceAnnotation: "resource",
464 ManagementConflictObjectAnnotation: "",
465 MetadataMappingLabels: "labels_field",
466 LabelsFieldIsMutable: false,
467 ExpectedObjectAnnotation: "none",
468 ShouldSucceed: true,
469 },
470 {
471 Name: "resource policy on object should require labels support",
472 ManagementConflictNamespaceAnnotation: "",
473 ManagementConflictObjectAnnotation: "resource",
474 MetadataMappingLabels: "",
475 ExpectedObjectAnnotation: "resource",
476 ShouldSucceed: false,
477 },
478 {
479 Name: "resource policy on object should require mutable labels",
480 ManagementConflictNamespaceAnnotation: "",
481 ManagementConflictObjectAnnotation: "resource",
482 MetadataMappingLabels: "labels_field",
483 LabelsFieldIsMutable: false,
484 ExpectedObjectAnnotation: "resource",
485 ShouldSucceed: false,
486 },
487 {
488 Name: "invalid policy on namespace",
489 ManagementConflictNamespaceAnnotation: "invalid",
490 ManagementConflictObjectAnnotation: "",
491 MetadataMappingLabels: "",
492 ExpectedObjectAnnotation: "",
493 ShouldSucceed: false,
494 },
495 {
496 Name: "invalid policy on object",
497 ManagementConflictNamespaceAnnotation: "resource",
498 ManagementConflictObjectAnnotation: "invalid",
499 MetadataMappingLabels: "",
500 ExpectedObjectAnnotation: "invalid",
501 ShouldSucceed: false,
502 },
503 {
504 Name: "no value on namespace or resource with no labels support (i.e. default behavior when the resource doesn't support labels)",
505 ManagementConflictNamespaceAnnotation: "",
506 ManagementConflictObjectAnnotation: "",
507 MetadataMappingLabels: "",
508 ExpectedObjectAnnotation: "none",
509 ShouldSucceed: true,
510 },
511 {
512 Name: "no value on namespace or resource with immutable labels (i.e. default behavior when the resource doesn't support mutable labels)",
513 ManagementConflictNamespaceAnnotation: "",
514 ManagementConflictObjectAnnotation: "",
515 MetadataMappingLabels: "",
516 LabelsFieldIsMutable: false,
517 ExpectedObjectAnnotation: "none",
518 ShouldSucceed: true,
519 },
520 {
521 Name: "no value on namespace or resource with mutable labels (i.e. default behavior when the resource supports mutable labels)",
522 ManagementConflictNamespaceAnnotation: "",
523 ManagementConflictObjectAnnotation: "",
524 MetadataMappingLabels: "labels_value",
525 LabelsFieldIsMutable: true,
526 ExpectedObjectAnnotation: "none",
527 ShouldSucceed: true,
528 },
529 }
530 for _, tc := range tests {
531 t.Run(tc.Name, func(t *testing.T) {
532 ns := corev1.Namespace{}
533 ns.SetName("my-namespace")
534 ns.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictNamespaceAnnotation))
535 obj := unstructured.Unstructured{}
536 obj.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictObjectAnnotation))
537
538 fakeTFResourceName := "google_fake_resource"
539 fakeTFResource := &tfschema.Resource{
540 Schema: map[string]*tfschema.Schema{},
541 }
542 fakeTFLabelsField := tc.MetadataMappingLabels
543 if fakeTFLabelsField != "" {
544 fakeTFResource.Schema[fakeTFLabelsField] = &tfschema.Schema{
545 ForceNew: !tc.LabelsFieldIsMutable,
546 }
547 }
548 fakeTFProvider := &tfschema.Provider{
549 ResourcesMap: map[string]*tfschema.Resource{
550 fakeTFResourceName: fakeTFResource,
551 },
552 }
553 rc := corekccv1alpha1.ResourceConfig{
554 Name: fakeTFResourceName,
555 MetadataMapping: corekccv1alpha1.MetadataMapping{
556 Labels: fakeTFLabelsField,
557 },
558 }
559
560 err := k8s.ValidateOrDefaultManagementConflictPreventionAnnotationForTFBasedResource(&obj, &ns, &rc, fakeTFProvider.ResourcesMap)
561 if tc.ShouldSucceed != (err == nil) {
562 t.Fatalf("expected success to be '%v', instead got error mismsatch: %v", tc.ShouldSucceed, err)
563 }
564 value, ok := k8s.GetAnnotation(k8s.ManagementConflictPreventionPolicyFullyQualifiedAnnotation, &obj)
565 if ok || tc.ExpectedObjectAnnotation != "" {
566 if value != tc.ExpectedObjectAnnotation {
567 t.Fatalf("unexpected management conflict annotation value: got '%v', want '%v'", value, tc.ExpectedObjectAnnotation)
568 }
569 }
570 })
571 }
572 }
573
574 func TestValidateOrDefaultManagementConflictPreventionAnnotationForDCLBasedResource(t *testing.T) {
575 tests := []struct {
576 Name string
577 ManagementConflictNamespaceAnnotation string
578 ManagementConflictObjectAnnotation string
579 Schema *openapi.Schema
580 ExpectedObjectAnnotation string
581 ShouldSucceed bool
582 }{
583 {
584 Name: "none policy on namespace, empty on object",
585 ManagementConflictNamespaceAnnotation: "none",
586 ManagementConflictObjectAnnotation: "",
587 Schema: &openapi.Schema{
588 Type: "object",
589 Properties: map[string]*openapi.Schema{
590 "labels": &openapi.Schema{
591 Type: "string",
592 },
593 },
594 Extension: map[string]interface{}{
595 "x-dcl-labels": "labels",
596 },
597 },
598 ExpectedObjectAnnotation: "none",
599 ShouldSucceed: true,
600 },
601 {
602 Name: "none policy on namespace, resource on object",
603 ManagementConflictNamespaceAnnotation: "none",
604 ManagementConflictObjectAnnotation: "resource",
605 Schema: &openapi.Schema{
606 Type: "object",
607 Properties: map[string]*openapi.Schema{
608 "labels": &openapi.Schema{
609 Type: "string",
610 },
611 },
612 Extension: map[string]interface{}{
613 "x-dcl-labels": "labels",
614 },
615 },
616 ExpectedObjectAnnotation: "resource",
617 ShouldSucceed: true,
618 },
619 {
620 Name: "none policy on namespace, none on object",
621 ManagementConflictNamespaceAnnotation: "none",
622 ManagementConflictObjectAnnotation: "none",
623 Schema: &openapi.Schema{
624 Type: "object",
625 },
626 ExpectedObjectAnnotation: "none",
627 ShouldSucceed: true,
628 },
629 {
630 Name: "resource policy on namespace, empty on object",
631 ManagementConflictNamespaceAnnotation: "resource",
632 ManagementConflictObjectAnnotation: "",
633 Schema: &openapi.Schema{
634 Type: "object",
635 Properties: map[string]*openapi.Schema{
636 "labels": &openapi.Schema{
637 Type: "string",
638 },
639 },
640 Extension: map[string]interface{}{
641 "x-dcl-labels": "labels",
642 },
643 },
644 ExpectedObjectAnnotation: "resource",
645 ShouldSucceed: true,
646 },
647 {
648 Name: "resource policy on namespace, resource on object",
649 ManagementConflictNamespaceAnnotation: "resource",
650 ManagementConflictObjectAnnotation: "resource",
651 Schema: &openapi.Schema{
652 Type: "object",
653 Properties: map[string]*openapi.Schema{
654 "labels": &openapi.Schema{
655 Type: "string",
656 },
657 },
658 Extension: map[string]interface{}{
659 "x-dcl-labels": "labels",
660 },
661 },
662 ExpectedObjectAnnotation: "resource",
663 ShouldSucceed: true,
664 },
665 {
666 Name: "resource policy on namespace, none on object",
667 ManagementConflictNamespaceAnnotation: "resource",
668 ManagementConflictObjectAnnotation: "none",
669 Schema: &openapi.Schema{
670 Type: "object",
671 Properties: map[string]*openapi.Schema{
672 "labels": &openapi.Schema{
673 Type: "string",
674 },
675 },
676 Extension: map[string]interface{}{
677 "x-dcl-labels": "labels",
678 },
679 },
680 ExpectedObjectAnnotation: "none",
681 ShouldSucceed: true,
682 },
683 {
684 Name: "resource policy on namespace with no labels support should default to none",
685 ManagementConflictNamespaceAnnotation: "resource",
686 Schema: &openapi.Schema{
687 Type: "object",
688 },
689 ExpectedObjectAnnotation: "none",
690 ShouldSucceed: true,
691 },
692 {
693 Name: "resource policy on namespace with immutable labels should default to none",
694 ManagementConflictNamespaceAnnotation: "resource",
695 ManagementConflictObjectAnnotation: "",
696 Schema: &openapi.Schema{
697 Type: "object",
698 Properties: map[string]*openapi.Schema{
699 "labels": &openapi.Schema{
700 Type: "string",
701 Extension: map[string]interface{}{
702 "x-kubernetes-immutable": true,
703 },
704 },
705 },
706 Extension: map[string]interface{}{
707 "x-dcl-labels": "labels",
708 },
709 },
710 ExpectedObjectAnnotation: "none",
711 ShouldSucceed: true,
712 },
713 {
714 Name: "resource policy on object should require labels support",
715 ManagementConflictNamespaceAnnotation: "",
716 ManagementConflictObjectAnnotation: "resource",
717 Schema: &openapi.Schema{
718 Type: "object",
719 },
720 ExpectedObjectAnnotation: "resource",
721 ShouldSucceed: false,
722 },
723 {
724 Name: "resource policy on object should require mutable labels",
725 ManagementConflictNamespaceAnnotation: "",
726 ManagementConflictObjectAnnotation: "resource",
727 Schema: &openapi.Schema{
728 Type: "object",
729 Properties: map[string]*openapi.Schema{
730 "labels": &openapi.Schema{
731 Type: "string",
732 Extension: map[string]interface{}{
733 "x-kubernetes-immutable": true,
734 },
735 },
736 },
737 Extension: map[string]interface{}{
738 "x-dcl-labels": "labels",
739 },
740 },
741 ExpectedObjectAnnotation: "resource",
742 ShouldSucceed: false,
743 },
744 {
745 Name: "invalid policy on namespace",
746 ManagementConflictNamespaceAnnotation: "invalid",
747 ManagementConflictObjectAnnotation: "",
748 Schema: &openapi.Schema{
749 Type: "object",
750 },
751 ExpectedObjectAnnotation: "",
752 ShouldSucceed: false,
753 },
754 {
755 Name: "invalid policy on object",
756 ManagementConflictNamespaceAnnotation: "resource",
757 ManagementConflictObjectAnnotation: "invalid",
758 Schema: &openapi.Schema{
759 Type: "object",
760 },
761 ExpectedObjectAnnotation: "invalid",
762 ShouldSucceed: false,
763 },
764 {
765 Name: "no value on namespace or resource with no labels support (i.e. default behavior when the resource doesn't support labels)",
766 ManagementConflictNamespaceAnnotation: "",
767 ManagementConflictObjectAnnotation: "",
768 Schema: &openapi.Schema{
769 Type: "object",
770 },
771 ExpectedObjectAnnotation: "none",
772 ShouldSucceed: true,
773 },
774 {
775 Name: "no value on namespace or resource with immutable labels (i.e. default behavior when the resource doesn't support mutable labels)",
776 ManagementConflictNamespaceAnnotation: "",
777 ManagementConflictObjectAnnotation: "",
778 Schema: &openapi.Schema{
779 Type: "object",
780 Properties: map[string]*openapi.Schema{
781 "labels": &openapi.Schema{
782 Type: "string",
783 Extension: map[string]interface{}{
784 "x-kubernetes-immutable": true,
785 },
786 },
787 },
788 Extension: map[string]interface{}{
789 "x-dcl-labels": "labels",
790 },
791 },
792 ExpectedObjectAnnotation: "none",
793 ShouldSucceed: true,
794 },
795 {
796 Name: "no value on namespace or resource with mutable labels (i.e. default behavior when the resource supports mutable labels)",
797 ManagementConflictNamespaceAnnotation: "",
798 ManagementConflictObjectAnnotation: "",
799 Schema: &openapi.Schema{
800 Type: "object",
801 Properties: map[string]*openapi.Schema{
802 "labels": &openapi.Schema{
803 Type: "string",
804 },
805 },
806 Extension: map[string]interface{}{
807 "x-dcl-labels": "labels",
808 },
809 },
810 ExpectedObjectAnnotation: "none",
811 ShouldSucceed: true,
812 },
813 }
814 for _, tc := range tests {
815 t.Run(tc.Name, func(t *testing.T) {
816 ns := corev1.Namespace{}
817 ns.SetName("my-namespace")
818 ns.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictNamespaceAnnotation))
819 obj := unstructured.Unstructured{}
820 obj.SetAnnotations(newManagementConflictAnnotations(tc.ManagementConflictObjectAnnotation))
821
822 err := k8s.ValidateOrDefaultManagementConflictPreventionAnnotationForDCLBasedResource(&obj, &ns, tc.Schema)
823 if tc.ShouldSucceed != (err == nil) {
824 t.Fatalf("expected success to be '%v', instead got error mismsatch: %v", tc.ShouldSucceed, err)
825 }
826 value, ok := k8s.GetAnnotation(k8s.ManagementConflictPreventionPolicyFullyQualifiedAnnotation, &obj)
827 if ok || tc.ExpectedObjectAnnotation != "" {
828 if value != tc.ExpectedObjectAnnotation {
829 t.Fatalf("unexpected management conflict annotation value: got '%v', want '%v'", value, tc.ExpectedObjectAnnotation)
830 }
831 }
832 })
833 }
834 }
835
836 func TestGetManagementConflictPreventionAnnotationValue(t *testing.T) {
837 testCases := []struct {
838 Name string
839 Annotations map[string]string
840 ExpectedPolicy k8s.ManagementConflictPreventionPolicy
841 ShouldSucceed bool
842 }{
843 {
844 Name: "nil annotations should error",
845 Annotations: nil,
846 ExpectedPolicy: k8s.ManagementConflictPreventionPolicyNone,
847 ShouldSucceed: false,
848 },
849 {
850 Name: "missing annotation should error",
851 Annotations: make(map[string]string),
852 ExpectedPolicy: k8s.ManagementConflictPreventionPolicyNone,
853 ShouldSucceed: false,
854 },
855 {
856 Name: "invalid annotation should error",
857 Annotations: newManagementConflictAnnotations("my invalid policy name"),
858 ExpectedPolicy: k8s.ManagementConflictPreventionPolicyNone,
859 ShouldSucceed: false,
860 },
861 {
862 Name: "valid value should succeed",
863 Annotations: newManagementConflictAnnotations(k8s.ManagementConflictPreventionPolicyResource),
864 ExpectedPolicy: k8s.ManagementConflictPreventionPolicyResource,
865 ShouldSucceed: true,
866 },
867 }
868 for _, tc := range testCases {
869 t.Run(tc.Name, func(t *testing.T) {
870 obj := unstructured.Unstructured{}
871 obj.SetAnnotations(tc.Annotations)
872 policy, err := k8s.GetManagementConflictPreventionAnnotationValue(&obj)
873 if tc.ShouldSucceed != (err == nil) {
874 t.Fatalf("expected success to be '%v', instead got error mismatch: %v", tc.ShouldSucceed, err)
875 }
876 if policy != tc.ExpectedPolicy {
877 t.Fatalf("policy mismatch: got '%v', want '%v'", policy, tc.ExpectedPolicy)
878 }
879 })
880 }
881 }
882
883 func newManagementConflictAnnotations(policy string) map[string]string {
884 annotations := make(map[string]string)
885 if policy != "" {
886 annotations[k8s.ManagementConflictPreventionPolicyFullyQualifiedAnnotation] = policy
887 }
888 return annotations
889 }
890
891 func TestMain(m *testing.M) {
892 testmain.TestMainForUnitTests(m, &mgr)
893 }
894
View as plain text