1
2
3
4 package prune
5
6 import (
7 "context"
8 "fmt"
9 "strings"
10 "testing"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14 apierrors "k8s.io/apimachinery/pkg/api/errors"
15 "k8s.io/apimachinery/pkg/api/meta"
16 "k8s.io/apimachinery/pkg/api/meta/testrestmapper"
17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
19 "k8s.io/apimachinery/pkg/runtime"
20 "k8s.io/apimachinery/pkg/runtime/schema"
21 "k8s.io/apimachinery/pkg/util/sets"
22 "k8s.io/client-go/dynamic"
23 "k8s.io/client-go/dynamic/fake"
24 "k8s.io/kubectl/pkg/scheme"
25 "sigs.k8s.io/cli-utils/pkg/apply/cache"
26 "sigs.k8s.io/cli-utils/pkg/apply/event"
27 "sigs.k8s.io/cli-utils/pkg/apply/filter"
28 "sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
29 "sigs.k8s.io/cli-utils/pkg/common"
30 "sigs.k8s.io/cli-utils/pkg/inventory"
31 "sigs.k8s.io/cli-utils/pkg/object"
32 "sigs.k8s.io/cli-utils/pkg/testutil"
33 )
34
35 var testNamespace = "test-inventory-namespace"
36 var inventoryObjName = "test-inventory-obj"
37 var podName = "pod-1"
38 var pdbName = "pdb"
39
40 var testInventoryLabel = "test-app-label"
41
42 var inventoryObj = &unstructured.Unstructured{
43 Object: map[string]interface{}{
44 "apiVersion": "v1",
45 "kind": "ConfigMap",
46 "metadata": map[string]interface{}{
47 "name": inventoryObjName,
48 "namespace": testNamespace,
49 "labels": map[string]interface{}{
50 common.InventoryLabel: testInventoryLabel,
51 },
52 },
53 },
54 }
55
56 var namespace = &unstructured.Unstructured{
57 Object: map[string]interface{}{
58 "apiVersion": "v1",
59 "kind": "Namespace",
60 "metadata": map[string]interface{}{
61 "name": testNamespace,
62 "uid": "uid-namespace",
63 "annotations": map[string]interface{}{
64 "config.k8s.io/owning-inventory": testInventoryLabel,
65 },
66 },
67 },
68 }
69
70 var pod = &unstructured.Unstructured{
71 Object: map[string]interface{}{
72 "apiVersion": "v1",
73 "kind": "Pod",
74 "metadata": map[string]interface{}{
75 "name": podName,
76 "namespace": testNamespace,
77 "uid": "pod-uid",
78 "annotations": map[string]interface{}{
79 "config.k8s.io/owning-inventory": testInventoryLabel,
80 },
81 },
82 },
83 }
84
85 var pdb = &unstructured.Unstructured{
86 Object: map[string]interface{}{
87 "apiVersion": "policy/v1beta1",
88 "kind": "PodDisruptionBudget",
89 "metadata": map[string]interface{}{
90 "name": pdbName,
91 "namespace": testNamespace,
92 "uid": "uid2",
93 "annotations": map[string]interface{}{
94 "config.k8s.io/owning-inventory": testInventoryLabel,
95 },
96 },
97 },
98 }
99
100 var pdbDeleteFailure = &unstructured.Unstructured{
101 Object: map[string]interface{}{
102 "apiVersion": "policy/v1beta1",
103 "kind": "PodDisruptionBudget",
104 "metadata": map[string]interface{}{
105 "name": pdbName + "delete-failure",
106 "namespace": testNamespace,
107 "uid": "uid2",
108 "annotations": map[string]interface{}{
109 "config.k8s.io/owning-inventory": testInventoryLabel,
110 },
111 },
112 },
113 }
114
115 var crontabCRManifest = `
116 apiVersion: "stable.example.com/v1"
117 kind: CronTab
118 metadata:
119 name: cron-tab-01
120 namespace: test-namespace
121 `
122
123
124
125 func createInventoryInfo(children ...*unstructured.Unstructured) inventory.Info {
126 inventoryObjCopy := inventoryObj.DeepCopy()
127 wrappedInv := inventory.WrapInventoryObj(inventoryObjCopy)
128 objs := object.UnstructuredSetToObjMetadataSet(children)
129 if err := wrappedInv.Store(objs, nil); err != nil {
130 return nil
131 }
132 obj, err := wrappedInv.GetObject()
133 if err != nil {
134 return nil
135 }
136 return inventory.WrapInventoryInfoObj(obj)
137 }
138
139
140 var podDeletionPrevention = &unstructured.Unstructured{
141 Object: map[string]interface{}{
142 "apiVersion": "v1",
143 "kind": "Pod",
144 "metadata": map[string]interface{}{
145 "name": "test-prevent-delete",
146 "namespace": testNamespace,
147 "annotations": map[string]interface{}{
148 common.OnRemoveAnnotation: common.OnRemoveKeep,
149 inventory.OwningInventoryKey: testInventoryLabel,
150 },
151 "uid": "prevent-delete",
152 },
153 },
154 }
155
156 var pdbDeletePreventionManifest = `
157 apiVersion: "policy/v1beta1"
158 kind: PodDisruptionBudget
159 metadata:
160 name: pdb-delete-prevention
161 namespace: test-namespace
162 uid: uid2
163 annotations:
164 client.lifecycle.config.k8s.io/deletion: detach
165 config.k8s.io/owning-inventory: test-app-label
166 `
167
168
169 var (
170 defaultOptions = Options{
171 DryRunStrategy: common.DryRunNone,
172 PropagationPolicy: metav1.DeletePropagationBackground,
173 }
174 defaultOptionsDestroy = Options{
175 DryRunStrategy: common.DryRunNone,
176 PropagationPolicy: metav1.DeletePropagationBackground,
177 Destroy: true,
178 }
179 clientDryRunOptions = Options{
180 DryRunStrategy: common.DryRunClient,
181 PropagationPolicy: metav1.DeletePropagationBackground,
182 }
183 )
184
185 func TestPrune(t *testing.T) {
186 tests := map[string]struct {
187 clusterObjs []*unstructured.Unstructured
188 pruneObjs []*unstructured.Unstructured
189 pruneFilters []filter.ValidationFilter
190 options Options
191 expectedEvents []event.Event
192 expectedSkipped object.ObjMetadataSet
193 expectedFailed object.ObjMetadataSet
194 expectedAbandoned object.ObjMetadataSet
195 }{
196 "No pruned objects; no prune/delete events": {
197 clusterObjs: []*unstructured.Unstructured{},
198 pruneObjs: []*unstructured.Unstructured{},
199 options: defaultOptions,
200 expectedEvents: nil,
201 },
202 "One successfully pruned object": {
203 clusterObjs: []*unstructured.Unstructured{pod},
204 pruneObjs: []*unstructured.Unstructured{pod},
205 options: defaultOptions,
206 expectedEvents: []event.Event{
207 {
208 Type: event.PruneType,
209 PruneEvent: event.PruneEvent{
210 Identifier: object.UnstructuredToObjMetadata(pod),
211 Status: event.PruneSuccessful,
212 Object: pod,
213 },
214 },
215 },
216 },
217 "Multiple successfully pruned object": {
218 clusterObjs: []*unstructured.Unstructured{pod, pdb, namespace},
219 pruneObjs: []*unstructured.Unstructured{pod, pdb, namespace},
220 options: defaultOptions,
221 expectedEvents: []event.Event{
222 {
223 Type: event.PruneType,
224 PruneEvent: event.PruneEvent{
225 Identifier: object.UnstructuredToObjMetadata(pod),
226 Status: event.PruneSuccessful,
227 Object: pod,
228 },
229 },
230 {
231 Type: event.PruneType,
232 PruneEvent: event.PruneEvent{
233 Identifier: object.UnstructuredToObjMetadata(pdb),
234 Status: event.PruneSuccessful,
235 Object: pdb,
236 },
237 },
238 {
239 Type: event.PruneType,
240 PruneEvent: event.PruneEvent{
241 Identifier: object.UnstructuredToObjMetadata(namespace),
242 Status: event.PruneSuccessful,
243 Object: namespace,
244 },
245 },
246 },
247 },
248 "One successfully deleted object": {
249 clusterObjs: []*unstructured.Unstructured{pod},
250 pruneObjs: []*unstructured.Unstructured{pod},
251 options: defaultOptionsDestroy,
252 expectedEvents: []event.Event{
253 {
254 Type: event.DeleteType,
255 DeleteEvent: event.DeleteEvent{
256 Identifier: object.UnstructuredToObjMetadata(pod),
257 Status: event.DeleteSuccessful,
258 Object: pod,
259 },
260 },
261 },
262 },
263 "Multiple successfully deleted objects": {
264 clusterObjs: []*unstructured.Unstructured{pod, pdb, namespace},
265 pruneObjs: []*unstructured.Unstructured{pod, pdb, namespace},
266 options: defaultOptionsDestroy,
267 expectedEvents: []event.Event{
268 {
269 Type: event.DeleteType,
270 DeleteEvent: event.DeleteEvent{
271 Identifier: object.UnstructuredToObjMetadata(pod),
272 Status: event.DeleteSuccessful,
273 Object: pod,
274 },
275 },
276 {
277 Type: event.DeleteType,
278 DeleteEvent: event.DeleteEvent{
279 Identifier: object.UnstructuredToObjMetadata(pdb),
280 Status: event.DeleteSuccessful,
281 Object: pdb,
282 },
283 },
284 {
285 Type: event.DeleteType,
286 DeleteEvent: event.DeleteEvent{
287 Identifier: object.UnstructuredToObjMetadata(namespace),
288 Status: event.DeleteSuccessful,
289 Object: namespace,
290 },
291 },
292 },
293 },
294 "Client dry run still pruned event": {
295 clusterObjs: []*unstructured.Unstructured{pod},
296 pruneObjs: []*unstructured.Unstructured{pod},
297 options: clientDryRunOptions,
298 expectedEvents: []event.Event{
299 {
300 Type: event.PruneType,
301 PruneEvent: event.PruneEvent{
302 Identifier: object.UnstructuredToObjMetadata(pod),
303 Status: event.PruneSuccessful,
304 Object: pod,
305 },
306 },
307 },
308 },
309 "Server dry run still deleted event": {
310 clusterObjs: []*unstructured.Unstructured{pod},
311 pruneObjs: []*unstructured.Unstructured{pod},
312 options: Options{
313 DryRunStrategy: common.DryRunServer,
314 PropagationPolicy: metav1.DeletePropagationBackground,
315 Destroy: true,
316 },
317 expectedEvents: []event.Event{
318 {
319 Type: event.DeleteType,
320 DeleteEvent: event.DeleteEvent{
321 Identifier: object.UnstructuredToObjMetadata(pod),
322 Status: event.DeleteSuccessful,
323 Object: pod,
324 },
325 },
326 },
327 },
328 "UID match means prune skipped": {
329 clusterObjs: []*unstructured.Unstructured{pod},
330 pruneObjs: []*unstructured.Unstructured{pod},
331 pruneFilters: []filter.ValidationFilter{
332 filter.CurrentUIDFilter{
333
334 CurrentUIDs: sets.NewString("pod-uid"),
335 },
336 },
337 options: defaultOptions,
338 expectedEvents: []event.Event{
339 {
340 Type: event.PruneType,
341 PruneEvent: event.PruneEvent{
342 Identifier: object.UnstructuredToObjMetadata(pod),
343 Status: event.PruneSkipped,
344 Object: pod,
345 Error: testutil.EqualError(&filter.ApplyPreventedDeletionError{
346 UID: "pod-uid",
347 }),
348 },
349 },
350 },
351 expectedSkipped: object.ObjMetadataSet{
352 object.UnstructuredToObjMetadata(pod),
353 },
354 },
355 "UID match for only one object one pruned, one skipped": {
356 clusterObjs: []*unstructured.Unstructured{pod, pdb},
357 pruneObjs: []*unstructured.Unstructured{pod, pdb},
358 pruneFilters: []filter.ValidationFilter{
359 filter.CurrentUIDFilter{
360
361 CurrentUIDs: sets.NewString("pod-uid"),
362 },
363 },
364 options: defaultOptions,
365 expectedEvents: []event.Event{
366 {
367 Type: event.PruneType,
368 PruneEvent: event.PruneEvent{
369 Identifier: object.UnstructuredToObjMetadata(pod),
370 Status: event.PruneSkipped,
371 Object: pod,
372 Error: testutil.EqualError(&filter.ApplyPreventedDeletionError{
373 UID: "pod-uid",
374 }),
375 },
376 },
377 {
378 Type: event.PruneType,
379 PruneEvent: event.PruneEvent{
380 Identifier: object.UnstructuredToObjMetadata(pdb),
381 Status: event.PruneSuccessful,
382 Object: pdb,
383 },
384 },
385 },
386 expectedSkipped: object.ObjMetadataSet{
387 object.UnstructuredToObjMetadata(pod),
388 },
389 },
390 "Prevent delete annotation equals prune skipped": {
391 clusterObjs: []*unstructured.Unstructured{
392 podDeletionPrevention,
393 testutil.Unstructured(t, pdbDeletePreventionManifest),
394 },
395 pruneObjs: []*unstructured.Unstructured{
396 podDeletionPrevention,
397 testutil.Unstructured(t, pdbDeletePreventionManifest),
398 },
399 pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}},
400 options: defaultOptions,
401 expectedEvents: []event.Event{
402 {
403 Type: event.PruneType,
404 PruneEvent: event.PruneEvent{
405 Identifier: object.UnstructuredToObjMetadata(podDeletionPrevention),
406 Status: event.PruneSkipped,
407 Object: testutil.Mutate(podDeletionPrevention.DeepCopy(),
408 testutil.DeleteOwningInv(t, testInventoryLabel)),
409 Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{
410 Annotation: common.OnRemoveAnnotation,
411 Value: common.OnRemoveKeep,
412 }),
413 },
414 },
415 {
416 Type: event.PruneType,
417 PruneEvent: event.PruneEvent{
418 Identifier: testutil.ToIdentifier(t, pdbDeletePreventionManifest),
419 Status: event.PruneSkipped,
420 Object: testutil.Unstructured(t, pdbDeletePreventionManifest,
421 testutil.DeleteOwningInv(t, testInventoryLabel)),
422 Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{
423 Annotation: common.LifecycleDeleteAnnotation,
424 Value: common.PreventDeletion,
425 }),
426 },
427 },
428 },
429 expectedSkipped: object.ObjMetadataSet{
430 object.UnstructuredToObjMetadata(podDeletionPrevention),
431 testutil.ToIdentifier(t, pdbDeletePreventionManifest),
432 },
433 expectedAbandoned: object.ObjMetadataSet{
434 object.UnstructuredToObjMetadata(podDeletionPrevention),
435 testutil.ToIdentifier(t, pdbDeletePreventionManifest),
436 },
437 },
438 "Prevent delete annotation equals delete skipped": {
439 clusterObjs: []*unstructured.Unstructured{
440 podDeletionPrevention,
441 testutil.Unstructured(t, pdbDeletePreventionManifest),
442 },
443 pruneObjs: []*unstructured.Unstructured{
444 podDeletionPrevention,
445 testutil.Unstructured(t, pdbDeletePreventionManifest),
446 },
447 pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}},
448 options: defaultOptionsDestroy,
449 expectedEvents: []event.Event{
450 {
451 Type: event.DeleteType,
452 DeleteEvent: event.DeleteEvent{
453 Identifier: object.UnstructuredToObjMetadata(podDeletionPrevention),
454 Status: event.DeleteSkipped,
455 Object: testutil.Mutate(podDeletionPrevention.DeepCopy(),
456 testutil.DeleteOwningInv(t, testInventoryLabel)),
457 Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{
458 Annotation: common.OnRemoveAnnotation,
459 Value: common.OnRemoveKeep,
460 }),
461 },
462 },
463 {
464 Type: event.DeleteType,
465 DeleteEvent: event.DeleteEvent{
466 Identifier: testutil.ToIdentifier(t, pdbDeletePreventionManifest),
467 Status: event.DeleteSkipped,
468 Object: testutil.Unstructured(t, pdbDeletePreventionManifest,
469 testutil.DeleteOwningInv(t, testInventoryLabel)),
470 Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{
471 Annotation: common.LifecycleDeleteAnnotation,
472 Value: common.PreventDeletion,
473 }),
474 },
475 },
476 },
477 expectedSkipped: object.ObjMetadataSet{
478 object.UnstructuredToObjMetadata(podDeletionPrevention),
479 testutil.ToIdentifier(t, pdbDeletePreventionManifest),
480 },
481 expectedAbandoned: object.ObjMetadataSet{
482 object.UnstructuredToObjMetadata(podDeletionPrevention),
483 testutil.ToIdentifier(t, pdbDeletePreventionManifest),
484 },
485 },
486 "Prevent delete annotation, one skipped, one pruned": {
487 clusterObjs: []*unstructured.Unstructured{podDeletionPrevention, pod},
488 pruneObjs: []*unstructured.Unstructured{podDeletionPrevention, pod},
489 pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}},
490 options: defaultOptions,
491 expectedEvents: []event.Event{
492 {
493 Type: event.PruneType,
494 PruneEvent: event.PruneEvent{
495 Identifier: object.UnstructuredToObjMetadata(podDeletionPrevention),
496 Status: event.PruneSkipped,
497 Object: testutil.Mutate(podDeletionPrevention.DeepCopy(),
498 testutil.DeleteOwningInv(t, testInventoryLabel)),
499 Error: testutil.EqualError(&filter.AnnotationPreventedDeletionError{
500 Annotation: common.OnRemoveAnnotation,
501 Value: common.OnRemoveKeep,
502 }),
503 },
504 },
505 {
506 Type: event.PruneType,
507 PruneEvent: event.PruneEvent{
508 Status: event.PruneSuccessful,
509 Identifier: object.UnstructuredToObjMetadata(pod),
510 Object: pod,
511 },
512 },
513 },
514 expectedSkipped: object.ObjMetadataSet{
515 object.UnstructuredToObjMetadata(podDeletionPrevention),
516 },
517 expectedAbandoned: object.ObjMetadataSet{
518 object.UnstructuredToObjMetadata(podDeletionPrevention),
519 },
520 },
521 "Namespace prune skipped": {
522 clusterObjs: []*unstructured.Unstructured{namespace},
523 pruneObjs: []*unstructured.Unstructured{namespace},
524 pruneFilters: []filter.ValidationFilter{
525 filter.LocalNamespacesFilter{
526 LocalNamespaces: sets.NewString(namespace.GetName()),
527 },
528 },
529 options: defaultOptions,
530 expectedEvents: []event.Event{
531 {
532 Type: event.PruneType,
533 PruneEvent: event.PruneEvent{
534 Identifier: object.UnstructuredToObjMetadata(namespace),
535 Status: event.PruneSkipped,
536 Object: namespace,
537 Error: testutil.EqualError(&filter.NamespaceInUseError{
538 Namespace: namespace.GetName(),
539 }),
540 },
541 },
542 },
543 expectedSkipped: object.ObjMetadataSet{
544 object.UnstructuredToObjMetadata(namespace),
545 },
546 },
547 "Deletion of already deleted object": {
548 clusterObjs: []*unstructured.Unstructured{},
549 pruneObjs: []*unstructured.Unstructured{pod},
550 options: defaultOptionsDestroy,
551 expectedEvents: []event.Event{
552 {
553 Type: event.DeleteType,
554 DeleteEvent: event.DeleteEvent{
555 Identifier: object.UnstructuredToObjMetadata(pod),
556 Status: event.DeleteSuccessful,
557 Object: pod,
558 },
559 },
560 },
561 },
562 }
563
564 for name, tc := range tests {
565 t.Run(name, func(t *testing.T) {
566
567 clusterObjs := make([]runtime.Object, 0, len(tc.clusterObjs))
568 for _, obj := range tc.clusterObjs {
569 clusterObjs = append(clusterObjs, obj)
570 }
571 pruneIds := object.UnstructuredSetToObjMetadataSet(tc.pruneObjs)
572 po := Pruner{
573 InvClient: inventory.NewFakeClient(pruneIds),
574 Client: fake.NewSimpleDynamicClient(scheme.Scheme, clusterObjs...),
575 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
576 scheme.Scheme.PrioritizedVersionsAllGroups()...),
577 }
578
579
580 eventChannel := make(chan event.Event, len(tc.pruneObjs)+1)
581 resourceCache := cache.NewResourceCacheMap()
582 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
583 taskName := "test-0"
584 err := func() error {
585 defer close(eventChannel)
586
587 return po.Prune(tc.pruneObjs, tc.pruneFilters, taskContext, taskName, tc.options)
588 }()
589
590 if err != nil {
591 t.Fatalf("Unexpected error during Prune(): %#v", err)
592 }
593 var actualEvents []event.Event
594 for e := range eventChannel {
595 actualEvents = append(actualEvents, e)
596 }
597
598 for i := range tc.expectedEvents {
599 switch tc.expectedEvents[i].Type {
600 case event.ApplyType:
601 tc.expectedEvents[i].ApplyEvent.GroupName = taskName
602 case event.DeleteType:
603 tc.expectedEvents[i].DeleteEvent.GroupName = taskName
604 case event.PruneType:
605 tc.expectedEvents[i].PruneEvent.GroupName = taskName
606 }
607 }
608
609 testutil.AssertEqual(t, tc.expectedEvents, actualEvents)
610
611 im := taskContext.InventoryManager()
612
613
614 for _, id := range tc.expectedFailed {
615 assert.Truef(t, im.IsFailedDelete(id), "Prune() should mark object as failed: %s", id)
616 }
617 for _, id := range pruneIds.Diff(tc.expectedFailed) {
618 assert.Falsef(t, im.IsFailedDelete(id), "Prune() should NOT mark object as failed: %s", id)
619 }
620
621 for _, id := range tc.expectedSkipped {
622 assert.Truef(t, im.IsSkippedDelete(id), "Prune() should mark object as skipped: %s", id)
623 }
624 for _, id := range pruneIds.Diff(tc.expectedSkipped) {
625 assert.Falsef(t, im.IsSkippedDelete(id), "Prune() should NOT mark object as skipped: %s", id)
626 }
627
628 for _, id := range tc.expectedAbandoned {
629 assert.Truef(t, taskContext.IsAbandonedObject(id), "Prune() should mark object as abandoned: %s", id)
630 }
631 for _, id := range pruneIds.Diff(tc.expectedAbandoned) {
632 assert.Falsef(t, taskContext.IsAbandonedObject(id), "Prune() should NOT mark object as abandoned: %s", id)
633 }
634 })
635 }
636 }
637
638 func TestPruneDeletionPrevention(t *testing.T) {
639 tests := map[string]struct {
640 pruneObj *unstructured.Unstructured
641 options Options
642 }{
643 "an object with the cli-utils.sigs.k8s.io/on-remove annotation (prune)": {
644 pruneObj: podDeletionPrevention,
645 options: defaultOptions,
646 },
647 "an object with the cli-utils.sigs.k8s.io/on-remove annotation (destroy)": {
648 pruneObj: podDeletionPrevention,
649 options: defaultOptionsDestroy,
650 },
651 "an object with the client.lifecycle.config.k8s.io/deletion annotation (prune)": {
652 pruneObj: testutil.Unstructured(t, pdbDeletePreventionManifest),
653 options: defaultOptions,
654 },
655 "an object with the client.lifecycle.config.k8s.io/deletion annotation (destroy)": {
656 pruneObj: testutil.Unstructured(t, pdbDeletePreventionManifest),
657 options: defaultOptionsDestroy,
658 },
659 }
660 for name, tc := range tests {
661 t.Run(name, func(t *testing.T) {
662 pruneID := object.UnstructuredToObjMetadata(tc.pruneObj)
663 po := Pruner{
664 InvClient: inventory.NewFakeClient(object.ObjMetadataSet{pruneID}),
665 Client: fake.NewSimpleDynamicClient(scheme.Scheme, tc.pruneObj),
666 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
667 scheme.Scheme.PrioritizedVersionsAllGroups()...),
668 }
669
670
671 eventChannel := make(chan event.Event, 2)
672 resourceCache := cache.NewResourceCacheMap()
673 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
674 err := func() error {
675 defer close(eventChannel)
676
677 return po.Prune([]*unstructured.Unstructured{tc.pruneObj}, []filter.ValidationFilter{filter.PreventRemoveFilter{}}, taskContext, "test-0", tc.options)
678 }()
679 require.NoError(t, err)
680
681
682 obj, err := po.getObject(pruneID)
683 require.NoError(t, err)
684
685 for annotation := range obj.GetAnnotations() {
686 if annotation == inventory.OwningInventoryKey {
687 t.Errorf("Prune() should remove the %s annotation", inventory.OwningInventoryKey)
688 break
689 }
690 }
691
692 im := taskContext.InventoryManager()
693
694 assert.Truef(t, taskContext.IsAbandonedObject(pruneID), "Prune() should mark object as abandoned")
695 assert.Truef(t, im.IsSkippedDelete(pruneID), "Prune() should mark object as skipped")
696 assert.Falsef(t, im.IsFailedDelete(pruneID), "Prune() should NOT mark object as failed")
697 })
698 }
699 }
700
701
702 type failureNamespaceClient struct {
703 dynamic.ResourceInterface
704 }
705
706 var _ dynamic.ResourceInterface = &failureNamespaceClient{}
707
708 func (c *failureNamespaceClient) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
709 if strings.Contains(name, "delete-failure") {
710 return fmt.Errorf("expected delete error")
711 }
712 return nil
713 }
714
715 func (c *failureNamespaceClient) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
716 if strings.Contains(name, "get-failure") {
717 return nil, fmt.Errorf("expected get error")
718 }
719 return pdb, nil
720 }
721
722 func TestPruneWithErrors(t *testing.T) {
723 tests := map[string]struct {
724 pruneObjs []*unstructured.Unstructured
725 destroy bool
726 expectedEvents []testutil.ExpEvent
727 }{
728 "Prune delete failure": {
729 pruneObjs: []*unstructured.Unstructured{pdbDeleteFailure},
730 expectedEvents: []testutil.ExpEvent{
731 {
732 EventType: event.PruneType,
733 PruneEvent: &testutil.ExpPruneEvent{
734 Identifier: object.UnstructuredToObjMetadata(pdbDeleteFailure),
735 Status: event.PruneFailed,
736 Error: fmt.Errorf("expected delete error"),
737 },
738 },
739 },
740 },
741 "Destroy delete failure": {
742 pruneObjs: []*unstructured.Unstructured{pdbDeleteFailure},
743 destroy: true,
744 expectedEvents: []testutil.ExpEvent{
745 {
746 EventType: event.DeleteType,
747 DeleteEvent: &testutil.ExpDeleteEvent{
748 Identifier: object.UnstructuredToObjMetadata(pdbDeleteFailure),
749 Status: event.DeleteFailed,
750 Error: fmt.Errorf("expected delete error"),
751 },
752 },
753 },
754 },
755 }
756 for name, tc := range tests {
757 t.Run(name, func(t *testing.T) {
758 pruneIds := object.UnstructuredSetToObjMetadataSet(tc.pruneObjs)
759 po := Pruner{
760 InvClient: inventory.NewFakeClient(pruneIds),
761
762 Client: &fakeDynamicClient{
763 resourceInterface: &failureNamespaceClient{},
764 },
765 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
766 scheme.Scheme.PrioritizedVersionsAllGroups()...),
767 }
768
769
770 eventChannel := make(chan event.Event, len(tc.pruneObjs))
771 resourceCache := cache.NewResourceCacheMap()
772 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
773 err := func() error {
774 defer close(eventChannel)
775 var opts Options
776 if tc.destroy {
777 opts = defaultOptionsDestroy
778 } else {
779 opts = defaultOptions
780 }
781
782 return po.Prune(tc.pruneObjs, []filter.ValidationFilter{}, taskContext, "test-0", opts)
783 }()
784 if err != nil {
785 t.Fatalf("Unexpected error during Prune(): %#v", err)
786 }
787 var actualEvents []event.Event
788 for e := range eventChannel {
789 actualEvents = append(actualEvents, e)
790 }
791 err = testutil.VerifyEvents(tc.expectedEvents, actualEvents)
792 assert.NoError(t, err)
793 })
794 }
795 }
796
797 func TestGetPruneObjs(t *testing.T) {
798 tests := map[string]struct {
799 localObjs []*unstructured.Unstructured
800 prevInventory []*unstructured.Unstructured
801 expectedObjs []*unstructured.Unstructured
802 }{
803 "no local objects, no inventory equals no prune objs": {
804 localObjs: []*unstructured.Unstructured{},
805 prevInventory: []*unstructured.Unstructured{},
806 expectedObjs: []*unstructured.Unstructured{},
807 },
808 "local objects, no inventory equals no prune objs": {
809 localObjs: []*unstructured.Unstructured{pod, pdb, namespace},
810 prevInventory: []*unstructured.Unstructured{},
811 expectedObjs: []*unstructured.Unstructured{},
812 },
813 "no local objects, with inventory equals all prune objs": {
814 localObjs: []*unstructured.Unstructured{},
815 prevInventory: []*unstructured.Unstructured{pod, pdb, namespace},
816 expectedObjs: []*unstructured.Unstructured{pod, pdb, namespace},
817 },
818 "set difference equals one prune object": {
819 localObjs: []*unstructured.Unstructured{pod, pdb},
820 prevInventory: []*unstructured.Unstructured{pdb, namespace},
821 expectedObjs: []*unstructured.Unstructured{namespace},
822 },
823 "local and inventory the same equals no prune objects": {
824 localObjs: []*unstructured.Unstructured{pod, pdb},
825 prevInventory: []*unstructured.Unstructured{pod, pdb},
826 expectedObjs: []*unstructured.Unstructured{},
827 },
828 "two prune objects": {
829 localObjs: []*unstructured.Unstructured{pdb},
830 prevInventory: []*unstructured.Unstructured{pod, pdb, namespace},
831 expectedObjs: []*unstructured.Unstructured{pod, namespace},
832 },
833 "skip pruning objects whose resource types are unrecognized by the cluster": {
834 localObjs: []*unstructured.Unstructured{pdb},
835 prevInventory: []*unstructured.Unstructured{testutil.Unstructured(t, crontabCRManifest), pdb, namespace},
836 expectedObjs: []*unstructured.Unstructured{namespace},
837 },
838 "local objs, inventory disjoint means inventory is pruned": {
839 localObjs: []*unstructured.Unstructured{pdb},
840 prevInventory: []*unstructured.Unstructured{pod, namespace},
841 expectedObjs: []*unstructured.Unstructured{pod, namespace},
842 },
843 }
844 for name, tc := range tests {
845 t.Run(name, func(t *testing.T) {
846 objs := make([]runtime.Object, 0, len(tc.prevInventory))
847 for _, obj := range tc.prevInventory {
848 objs = append(objs, obj)
849 }
850 po := Pruner{
851 InvClient: inventory.NewFakeClient(object.UnstructuredSetToObjMetadataSet(tc.prevInventory)),
852 Client: fake.NewSimpleDynamicClient(scheme.Scheme, objs...),
853 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
854 scheme.Scheme.PrioritizedVersionsAllGroups()...),
855 }
856 currentInventory := createInventoryInfo(tc.prevInventory...)
857 actualObjs, err := po.GetPruneObjs(currentInventory, tc.localObjs, Options{})
858 if err != nil {
859 t.Fatalf("unexpected error %s returned", err)
860 }
861 if len(tc.expectedObjs) != len(actualObjs) {
862 t.Fatalf("expected %d prune objs, got %d", len(tc.expectedObjs), len(actualObjs))
863 }
864 actualIds := object.UnstructuredSetToObjMetadataSet(actualObjs)
865 expectedIds := object.UnstructuredSetToObjMetadataSet(tc.expectedObjs)
866 if !object.ObjMetadataSetEquals(expectedIds, actualIds) {
867 t.Errorf("expected prune objects (%v), got (%v)", expectedIds, actualIds)
868 }
869 })
870 }
871 }
872
873 func TestGetObject_NoMatchError(t *testing.T) {
874 po := Pruner{
875 Client: fake.NewSimpleDynamicClient(scheme.Scheme, pod, namespace),
876 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
877 scheme.Scheme.PrioritizedVersionsAllGroups()...),
878 }
879 _, err := po.getObject(testutil.ToIdentifier(t, crontabCRManifest))
880 if err == nil {
881 t.Fatalf("expected GetObject() to return a NoKindMatchError, got nil")
882 }
883 if !meta.IsNoMatchError(err) {
884 t.Fatalf("expected GetObject() to return a NoKindMatchError, got %v", err)
885 }
886 }
887
888 func TestGetObject_NotFoundError(t *testing.T) {
889 po := Pruner{
890 Client: fake.NewSimpleDynamicClient(scheme.Scheme, pod, namespace),
891 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
892 scheme.Scheme.PrioritizedVersionsAllGroups()...),
893 }
894 id := object.UnstructuredToObjMetadata(pdb)
895 _, err := po.getObject(id)
896 if err == nil {
897 t.Fatalf("expected GetObject() to return a NotFound error, got nil")
898 }
899 if !apierrors.IsNotFound(err) {
900 t.Fatalf("expected GetObject() to return a NotFound error, got %v", err)
901 }
902 }
903
904 func TestHandleDeletePrevention(t *testing.T) {
905 obj := testutil.Unstructured(t, pdbDeletePreventionManifest)
906 po := Pruner{
907 Client: fake.NewSimpleDynamicClient(scheme.Scheme, obj, namespace),
908 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
909 scheme.Scheme.PrioritizedVersionsAllGroups()...),
910 }
911 var err error
912 obj, err = po.removeInventoryAnnotation(obj)
913 if err != nil {
914 t.Fatalf("unexpected error %s returned", err)
915 }
916
917 annotations := obj.GetAnnotations()
918 if annotations != nil {
919 if _, ok := annotations[inventory.OwningInventoryKey]; ok {
920 t.Fatalf("expected handleDeletePrevention() to remove the %q annotation", inventory.OwningInventoryKey)
921 }
922 }
923
924
925 obj, err = po.getObject(testutil.ToIdentifier(t, pdbDeletePreventionManifest))
926 if err != nil {
927 t.Fatalf("unexpected error %s returned", err)
928 }
929
930 annotations = obj.GetAnnotations()
931 if annotations != nil {
932 if _, ok := annotations[inventory.OwningInventoryKey]; ok {
933 t.Fatalf("expected handleDeletePrevention() to remove the %q annotation", inventory.OwningInventoryKey)
934 }
935 }
936 }
937
938 type optionsCaptureNamespaceClient struct {
939 dynamic.ResourceInterface
940 options metav1.DeleteOptions
941 }
942
943 var _ dynamic.ResourceInterface = &optionsCaptureNamespaceClient{}
944
945 func (c *optionsCaptureNamespaceClient) Delete(_ context.Context, _ string, options metav1.DeleteOptions, _ ...string) error {
946 c.options = options
947 return nil
948 }
949
950 func TestPrune_PropagationPolicy(t *testing.T) {
951 testCases := map[string]struct {
952 propagationPolicy metav1.DeletionPropagation
953 }{
954 "background propagation policy": {
955 propagationPolicy: metav1.DeletePropagationBackground,
956 },
957 "foreground propagation policy": {
958 propagationPolicy: metav1.DeletePropagationForeground,
959 },
960 }
961 for name, tc := range testCases {
962 t.Run(name, func(t *testing.T) {
963 captureClient := &optionsCaptureNamespaceClient{}
964 po := Pruner{
965 InvClient: inventory.NewFakeClient(object.ObjMetadataSet{}),
966 Client: &fakeDynamicClient{
967 resourceInterface: captureClient,
968 },
969 Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme,
970 scheme.Scheme.PrioritizedVersionsAllGroups()...),
971 }
972
973 eventChannel := make(chan event.Event, 1)
974 resourceCache := cache.NewResourceCacheMap()
975 taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
976 err := po.Prune([]*unstructured.Unstructured{pdb}, []filter.ValidationFilter{}, taskContext, "test-0", Options{
977 PropagationPolicy: tc.propagationPolicy,
978 })
979 assert.NoError(t, err)
980 require.NotNil(t, captureClient.options.PropagationPolicy)
981 assert.Equal(t, tc.propagationPolicy, *captureClient.options.PropagationPolicy)
982 })
983 }
984 }
985
986 type fakeDynamicClient struct {
987 resourceInterface dynamic.ResourceInterface
988 }
989
990 var _ dynamic.Interface = &fakeDynamicClient{}
991
992 func (c *fakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
993 return &fakeDynamicResourceClient{
994 resourceInterface: c.resourceInterface,
995 NamespaceableResourceInterface: fake.NewSimpleDynamicClient(scheme.Scheme).Resource(resource),
996 }
997 }
998
999 type fakeDynamicResourceClient struct {
1000 dynamic.NamespaceableResourceInterface
1001 resourceInterface dynamic.ResourceInterface
1002 }
1003
1004 func (c *fakeDynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface {
1005 return c.resourceInterface
1006 }
1007
View as plain text