diff --git a/examples/alphaTestExamples/pruneAndDelete.md b/examples/alphaTestExamples/pruneAndDelete.md index f08167823..7ee9f00cc 100644 --- a/examples/alphaTestExamples/pruneAndDelete.md +++ b/examples/alphaTestExamples/pruneAndDelete.md @@ -34,10 +34,14 @@ function expectedOutputLine() { } ``` -In this example we will just use two ConfigMap resources for simplicity, but -of course any type of resource can be used. On one of our ConfigMaps, we add the -**cli-utils.sigs.k8s.io/on-remove** annotation with the value of **keep**. This -annotation tells the kapply tool that this resource should not be deleted, even +In this example we will just use three ConfigMap resources for simplicity, but +of course any type of resource can be used. + +- the first ConfigMap resource does not have any annotations; +- the second ConfigMap resource has the **cli-utils.sigs.k8s.io/on-remove** annotation with the value of **keep**; +- the third ConfigMap resource has the **client.lifecycle.config.k8s.io/deletion** annotation with the value of **detach**. + +These two annotations tell the kapply tool that a resource should not be deleted, even if it would otherwise be pruned or deleted with the destroy command. @@ -53,7 +57,7 @@ data: EOF ``` -This ConfigMap includes the lifecycle directive annotation +This ConfigMap includes the **cli-utils.sigs.k8s.io/on-remove** annotation ``` @@ -70,6 +74,24 @@ data: EOF ``` + +This ConfigMap includes the **client.lifecycle.config.k8s.io/deletion** annotation + + +``` +cat <$BASE/configMap3.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: thirdmap + annotations: + client.lifecycle.config.k8s.io/deletion: detach +data: + artist: Husker Du + album: New Day Rising +EOF +``` + ## Run end-to-end tests The following requires installation of [kind]. @@ -90,14 +112,14 @@ expectedOutputLine "namespace: default is used for inventory object" ``` -Apply both resources to the cluster. +Apply the three resources to the cluster. ``` kapply apply $BASE --reconcile-timeout=1m > $OUTPUT/status ``` Use the preview command to show what will happen if we run destroy. This should -show that the second ConfigMap will not be deleted even when using the destroy +show that secondmap and thirdmap will not be deleted even when using the destroy command. ``` @@ -106,10 +128,12 @@ kapply preview --destroy $BASE > $OUTPUT/status expectedOutputLine "configmap/firstmap deleted (preview)" expectedOutputLine "configmap/secondmap delete skipped (preview)" + +expectedOutputLine "configmap/thirdmap delete skipped (preview)" ``` -We run the destroy command and see that the resource without the annotation -has been deleted, while the resource with the annotation is still in the +We run the destroy command and see that the resource without the annotations (firstmap) +has been deleted, while the resources with the annotations (secondmap and thirdmap) are still in the cluster. ``` @@ -119,45 +143,58 @@ expectedOutputLine "configmap/firstmap deleted" expectedOutputLine "configmap/secondmap delete skipped" -expectedOutputLine "1 resource(s) deleted, 1 skipped" +expectedOutputLine "configmap/thirdmap delete skipped" + +expectedOutputLine "1 resource(s) deleted, 2 skipped" expectedNotFound "resource(s) pruned" kubectl get cm --no-headers | awk '{print $1}' > $OUTPUT/status expectedOutputLine "secondmap" -``` +kubectl get cm --no-headers | awk '{print $1}' > $OUTPUT/status +expectedOutputLine "thirdmap" +``` Apply the resources back to the cluster so we can demonstrate the lifecycle directive with pruning. ``` -kapply apply $BASE --reconcile-timeout=1m > $OUTPUT/status +kapply apply $BASE --inventory-policy=adopt --reconcile-timeout=1m > $OUTPUT/status ``` -Delete the manifest for the second configmap +Delete the manifest for secondmap and thirdmap ``` rm $BASE/configMap2.yaml + +rm $BASE/configMap3.yaml ``` -Run preview to see that while secondmap would normally be pruned, it +Run preview to see that while secondmap and thirdmap would normally be pruned, they will instead be skipped due to the lifecycle directive. ``` kapply preview $BASE > $OUTPUT/status expectedOutputLine "configmap/secondmap prune skipped (preview)" + +expectedOutputLine "configmap/thirdmap prune skipped (preview)" ``` -Run apply and verify that secondmap is still in the cluster. +Run apply and verify that secondmap and thirdmap are still in the cluster. ``` kapply apply $BASE > $OUTPUT/status expectedOutputLine "configmap/secondmap prune skipped" +expectedOutputLine "configmap/thirdmap prune skipped" + kubectl get cm --no-headers | awk '{print $1}' > $OUTPUT/status expectedOutputLine "secondmap" +kubectl get cm --no-headers | awk '{print $1}' > $OUTPUT/status +expectedOutputLine "thirdmap" + kind delete cluster; ``` diff --git a/pkg/apply/event/actiongroupeventtype_string.go b/pkg/apply/event/actiongroupeventtype_string.go index ac610a2cb..22835dd6f 100644 --- a/pkg/apply/event/actiongroupeventtype_string.go +++ b/pkg/apply/event/actiongroupeventtype_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=ActionGroupEventType"; DO NOT EDIT. package event diff --git a/pkg/apply/event/applyeventoperation_string.go b/pkg/apply/event/applyeventoperation_string.go index edfbad0e6..f0b363c9c 100644 --- a/pkg/apply/event/applyeventoperation_string.go +++ b/pkg/apply/event/applyeventoperation_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=ApplyEventOperation"; DO NOT EDIT. package event diff --git a/pkg/apply/event/deleteeventoperation_string.go b/pkg/apply/event/deleteeventoperation_string.go index 90bc851d6..307f5835e 100644 --- a/pkg/apply/event/deleteeventoperation_string.go +++ b/pkg/apply/event/deleteeventoperation_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=DeleteEventOperation"; DO NOT EDIT. package event diff --git a/pkg/apply/event/pruneeventoperation_string.go b/pkg/apply/event/pruneeventoperation_string.go index 8fbee71f3..ffdcaf449 100644 --- a/pkg/apply/event/pruneeventoperation_string.go +++ b/pkg/apply/event/pruneeventoperation_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=PruneEventOperation"; DO NOT EDIT. package event diff --git a/pkg/apply/event/resourceaction_string.go b/pkg/apply/event/resourceaction_string.go index 65800e533..6db29968f 100644 --- a/pkg/apply/event/resourceaction_string.go +++ b/pkg/apply/event/resourceaction_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=ResourceAction"; DO NOT EDIT. package event diff --git a/pkg/apply/event/type_string.go b/pkg/apply/event/type_string.go index b37aa8b67..b9fb0b4f5 100644 --- a/pkg/apply/event/type_string.go +++ b/pkg/apply/event/type_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=Type"; DO NOT EDIT. package event diff --git a/pkg/apply/prune/prune.go b/pkg/apply/prune/prune.go index 24a653ead..c0f2113aa 100644 --- a/pkg/apply/prune/prune.go +++ b/pkg/apply/prune/prune.go @@ -103,21 +103,42 @@ func (po *PruneOptions) Prune(pruneObjs []*unstructured.Unstructured, var filtered bool var reason string var err error - for _, filter := range pruneFilters { - klog.V(6).Infof("prune filter %s: %s", filter.Name(), pruneID) - filtered, reason, err = filter.Filter(pruneObj) + for _, pruneFilter := range pruneFilters { + klog.V(6).Infof("prune filter %s: %s", pruneFilter.Name(), pruneID) + filtered, reason, err = pruneFilter.Filter(pruneObj) if err != nil { if klog.V(5).Enabled() { - klog.Errorf("error during %s, (%s): %s", filter.Name(), pruneID, err) + klog.Errorf("error during %s, (%s): %s", pruneFilter.Name(), pruneID, err) } taskContext.EventChannel() <- eventFactory.CreateFailedEvent(pruneID, err) taskContext.CapturePruneFailure(pruneID) break } if filtered { - klog.V(4).Infof("prune filtered (filter: %q, resource: %q, reason: %q)", filter.Name(), pruneID, reason) + klog.V(4).Infof("prune filtered (filter: %q, resource: %q, reason: %q)", pruneFilter.Name(), pruneID, reason) + // pruneFailure indicates whether `taskContext.CapturePruneFailure` should be called. + pruneFailure := true + if _, ok := pruneFilter.(filter.PreventRemoveFilter); ok { + if o.DryRunStrategy.ClientOrServerDryRun() { + pruneFailure = false + } else { + err := po.handleDeletePrevention(pruneObj) + if err != nil { + if klog.V(4).Enabled() { + klog.Errorf("Failed to remove the %q annotation from %s: %v", inventory.OwningInventoryKey, pruneID, err) + } + taskContext.EventChannel() <- eventFactory.CreateFailedEvent(pruneID, err) + taskContext.CapturePruneFailure(pruneID) + break + } else { + pruneFailure = false + } + } + } taskContext.EventChannel() <- eventFactory.CreateSkippedEvent(pruneObj, reason) - taskContext.CapturePruneFailure(pruneID) + if pruneFailure { + taskContext.CapturePruneFailure(pruneID) + } break } } @@ -153,6 +174,26 @@ func (po *PruneOptions) Prune(pruneObjs []*unstructured.Unstructured, return nil } +// handleDeletePrevention removes the `config.k8s.io/owning-inventory` annotation from pruneObj. +func (po *PruneOptions) handleDeletePrevention(pruneObj *unstructured.Unstructured) error { + pruneID := object.UnstructuredToObjMetaOrDie(pruneObj) + annotations := pruneObj.GetAnnotations() + if annotations != nil { + if _, ok := annotations[inventory.OwningInventoryKey]; ok { + klog.V(4).Infof("remove the %q annotation from the object %s", inventory.OwningInventoryKey, pruneID) + delete(annotations, inventory.OwningInventoryKey) + pruneObj.SetAnnotations(annotations) + namespacedClient, err := po.namespacedClient(pruneID) + if err != nil { + return err + } + _, err = namespacedClient.Update(context.TODO(), pruneObj, metav1.UpdateOptions{}) + return err + } + } + return nil +} + // GetPruneObjs calculates the set of prune objects, and retrieves them // from the cluster. Set of prune objects equals the set of inventory // objects minus the set of currently applied objects. Returns an error diff --git a/pkg/apply/prune/prune_test.go b/pkg/apply/prune/prune_test.go index 72a91a5c8..8b3c27a36 100644 --- a/pkg/apply/prune/prune_test.go +++ b/pkg/apply/prune/prune_test.go @@ -136,8 +136,8 @@ func createInventoryInfo(children ...*unstructured.Unstructured) inventory.Inven return inventory.WrapInventoryInfoObj(obj) } -// preventDelete object contains the "on-remove:keep" lifecycle directive. -var preventDelete = &unstructured.Unstructured{ +// podDeletionPrevention object contains the "on-remove:keep" lifecycle directive. +var podDeletionPrevention = &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", @@ -145,13 +145,26 @@ var preventDelete = &unstructured.Unstructured{ "name": "test-prevent-delete", "namespace": testNamespace, "annotations": map[string]interface{}{ - common.OnRemoveAnnotation: common.OnRemoveKeep, + common.OnRemoveAnnotation: common.OnRemoveKeep, + inventory.OwningInventoryKey: testInventoryLabel, }, "uid": "prevent-delete", }, }, } +var pdbDeletePreventionManifest = ` +apiVersion: "policy/v1beta1" +kind: PodDisruptionBudget +metadata: + name: pdb-delete-prevention + namespace: test-namespace + uid: uid2 + annotations: + client.lifecycle.config.k8s.io/deletion: detach + config.k8s.io/owning-inventory: test-app-label +` + // Options with different dry-run values. var ( defaultOptions = Options{ @@ -324,7 +337,7 @@ func TestPrune(t *testing.T) { }, }, "Prevent delete annotation equals prune skipped": { - pruneObjs: []*unstructured.Unstructured{preventDelete}, + pruneObjs: []*unstructured.Unstructured{podDeletionPrevention, testutil.Unstructured(t, pdbDeletePreventionManifest)}, pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}}, options: defaultOptions, expectedEvents: []testutil.ExpEvent{ @@ -334,10 +347,16 @@ func TestPrune(t *testing.T) { Operation: event.PruneSkipped, }, }, + { + EventType: event.PruneType, + PruneEvent: &testutil.ExpPruneEvent{ + Operation: event.PruneSkipped, + }, + }, }, }, "Prevent delete annotation equals delete skipped": { - pruneObjs: []*unstructured.Unstructured{preventDelete}, + pruneObjs: []*unstructured.Unstructured{podDeletionPrevention, testutil.Unstructured(t, pdbDeletePreventionManifest)}, pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}}, options: defaultOptionsDestroy, expectedEvents: []testutil.ExpEvent{ @@ -347,10 +366,16 @@ func TestPrune(t *testing.T) { Operation: event.DeleteSkipped, }, }, + { + EventType: event.DeleteType, + DeleteEvent: &testutil.ExpDeleteEvent{ + Operation: event.DeleteSkipped, + }, + }, }, }, "Prevent delete annotation, one skipped, one pruned": { - pruneObjs: []*unstructured.Unstructured{preventDelete, pod}, + pruneObjs: []*unstructured.Unstructured{podDeletionPrevention, pod}, pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}}, options: defaultOptions, expectedEvents: []testutil.ExpEvent{ @@ -428,6 +453,72 @@ func TestPrune(t *testing.T) { } } +func TestPruneDeletionPrevention(t *testing.T) { + tests := map[string]struct { + pruneObj *unstructured.Unstructured + options Options + }{ + "an object with the cli-utils.sigs.k8s.io/on-remove annotation (prune)": { + pruneObj: podDeletionPrevention, + options: defaultOptions, + }, + "an object with the cli-utils.sigs.k8s.io/on-remove annotation (destroy)": { + pruneObj: podDeletionPrevention, + options: defaultOptionsDestroy, + }, + "an object with the client.lifecycle.config.k8s.io/deletion annotation (prune)": { + pruneObj: testutil.Unstructured(t, pdbDeletePreventionManifest), + options: defaultOptions, + }, + "an object with the client.lifecycle.config.k8s.io/deletion annotation (destroy)": { + pruneObj: testutil.Unstructured(t, pdbDeletePreventionManifest), + options: defaultOptionsDestroy, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + pruneID, err := object.UnstructuredToObjMeta(tc.pruneObj) + require.NoError(t, err) + + po := PruneOptions{ + InvClient: inventory.NewFakeInventoryClient(object.ObjMetadataSet{pruneID}), + Client: fake.NewSimpleDynamicClient(scheme.Scheme, tc.pruneObj), + Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, + scheme.Scheme.PrioritizedVersionsAllGroups()...), + } + // The event channel can not block; make sure its bigger than all + // the events that can be put on it. + eventChannel := make(chan event.Event, 2) + resourceCache := cache.NewResourceCacheMap() + taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) + err = func() error { + defer close(eventChannel) + // Run the prune and validate. + return po.Prune([]*unstructured.Unstructured{tc.pruneObj}, []filter.ValidationFilter{filter.PreventRemoveFilter{}}, taskContext, "test-0", tc.options) + }() + + if err != nil { + t.Fatalf("Unexpected error during Prune(): %#v", err) + } + // verify that the object no longer has the annotation + obj, err := po.GetObject(pruneID) + if err != nil { + t.Fatalf("Unexpected error: %#v", err) + } + + hasOwningInventoryAnnotation := false + for annotation := range obj.GetAnnotations() { + if annotation == inventory.OwningInventoryKey { + hasOwningInventoryAnnotation = true + } + } + if hasOwningInventoryAnnotation { + t.Fatalf("Prune() should remove the %s annotation", inventory.OwningInventoryKey) + } + }) + } +} + // failureNamespaceClient wrappers around a namespaceClient with the overwriting to Get and Delete functions. type failureNamespaceClient struct { dynamic.ResourceInterface @@ -637,6 +728,30 @@ func TestGetObject_NotFoundError(t *testing.T) { } } +func TestHandleDeletePrevention(t *testing.T) { + obj := testutil.Unstructured(t, pdbDeletePreventionManifest) + po := PruneOptions{ + Client: fake.NewSimpleDynamicClient(scheme.Scheme, obj, namespace), + Mapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme, + scheme.Scheme.PrioritizedVersionsAllGroups()...), + } + if err := po.handleDeletePrevention(obj); err != nil { + t.Fatalf("unexpected error %s returned", err) + } + + // Get the object from the cluster and verify that the `config.k8s.io/owning-inventory` annotation is removed from the object. + liveObj, err := po.GetObject(testutil.ToIdentifier(t, pdbDeletePreventionManifest)) + if err != nil { + t.Fatalf("unexpected error %s returned", err) + } + annotations := liveObj.GetAnnotations() + if annotations != nil { + if _, ok := annotations[inventory.OwningInventoryKey]; ok { + t.Fatalf("expected handleDeletePrevention() to remove the %q annotation", inventory.OwningInventoryKey) + } + } +} + type optionsCaptureNamespaceClient struct { dynamic.ResourceInterface options metav1.DeleteOptions diff --git a/pkg/common/dryrunstrategy_string.go b/pkg/common/dryrunstrategy_string.go index a6e3823ff..2cb138cf2 100644 --- a/pkg/common/dryrunstrategy_string.go +++ b/pkg/common/dryrunstrategy_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=DryRunStrategy"; DO NOT EDIT. package common diff --git a/pkg/inventory/inventoryidmatchstatus_string.go b/pkg/inventory/inventoryidmatchstatus_string.go index ef3b0e3dd..31bc03726 100644 --- a/pkg/inventory/inventoryidmatchstatus_string.go +++ b/pkg/inventory/inventoryidmatchstatus_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=inventoryIDMatchStatus"; DO NOT EDIT. package inventory diff --git a/pkg/inventory/inventorypolicy_string.go b/pkg/inventory/inventorypolicy_string.go index 6053c4196..7e59a4b0d 100644 --- a/pkg/inventory/inventorypolicy_string.go +++ b/pkg/inventory/inventorypolicy_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=InventoryPolicy"; DO NOT EDIT. package inventory diff --git a/pkg/inventory/policy.go b/pkg/inventory/policy.go index 7c44b0945..ae42cf879 100644 --- a/pkg/inventory/policy.go +++ b/pkg/inventory/policy.go @@ -66,7 +66,8 @@ const ( AdoptAll ) -const owningInventoryKey = "config.k8s.io/owning-inventory" +// OwningInventoryKey is the annotation key indicating the inventory owning an object. +const OwningInventoryKey = "config.k8s.io/owning-inventory" // inventoryIDMatchStatus represents the result of comparing the // id from current inventory info and the inventory-id from a live object. @@ -81,7 +82,7 @@ const ( func InventoryIDMatch(inv InventoryInfo, obj *unstructured.Unstructured) inventoryIDMatchStatus { annotations := obj.GetAnnotations() - value, found := annotations[owningInventoryKey] + value, found := annotations[OwningInventoryKey] if !found { return Empty } @@ -101,7 +102,7 @@ func CanApply(inv InventoryInfo, obj *unstructured.Unstructured, policy Inventor if policy != InventoryPolicyMustMatch { return true, nil } - err := fmt.Errorf("can't adopt an object without the annotation %s", owningInventoryKey) + err := fmt.Errorf("can't adopt an object without the annotation %s", OwningInventoryKey) return false, NewNeedAdoptionError(err) case Match: return true, nil @@ -109,7 +110,7 @@ func CanApply(inv InventoryInfo, obj *unstructured.Unstructured, policy Inventor if policy == AdoptAll { return true, nil } - err := fmt.Errorf("can't apply the resource since its annotation %s is a different inventory object", owningInventoryKey) + err := fmt.Errorf("can't apply the resource since its annotation %s is a different inventory object", OwningInventoryKey) return false, NewInventoryOverlapError(err) } // shouldn't reach here @@ -137,7 +138,7 @@ func AddInventoryIDAnnotation(obj *unstructured.Unstructured, inv InventoryInfo) if annotations == nil { annotations = make(map[string]string) } - annotations[owningInventoryKey] = inv.ID() + annotations[OwningInventoryKey] = inv.ID() obj.SetAnnotations(annotations) } diff --git a/pkg/inventory/policy_test.go b/pkg/inventory/policy_test.go index de9ef91e9..7498b46a1 100644 --- a/pkg/inventory/policy_test.go +++ b/pkg/inventory/policy_test.go @@ -63,13 +63,13 @@ func TestInventoryIDMatch(t *testing.T) { }, { name: "matched", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, expected: Match, }, { name: "unmatched", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, expected: NoMatch, }, @@ -119,42 +119,42 @@ func TestCanApply(t *testing.T) { }, { name: "matched with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, policy: InventoryPolicyMustMatch, canApply: true, }, { name: "matched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, policy: AdoptIfNoInventory, canApply: true, }, { name: "matched with AloptAll", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, policy: AdoptAll, canApply: true, }, { name: "unmatched with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, policy: InventoryPolicyMustMatch, canApply: false, }, { name: "unmatched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, policy: AdoptIfNoInventory, canApply: false, }, { name: "unmatched with AdoptAll", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, policy: AdoptAll, canApply: true, @@ -205,42 +205,42 @@ func TestCanPrune(t *testing.T) { }, { name: "matched with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, policy: InventoryPolicyMustMatch, canPrune: true, }, { name: "matched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, policy: AdoptIfNoInventory, canPrune: true, }, { name: "matched with AloptAll", - obj: testObjectWithAnnotation(owningInventoryKey, "matched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "matched"), inv: &fakeInventoryInfo{id: "matched"}, policy: AdoptAll, canPrune: true, }, { name: "unmatched with InventoryPolicyMustMatch", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, policy: InventoryPolicyMustMatch, canPrune: false, }, { name: "unmatched with AdoptIfNoInventory", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, policy: AdoptIfNoInventory, canPrune: false, }, { name: "unmatched with AdoptAll", - obj: testObjectWithAnnotation(owningInventoryKey, "unmatched"), + obj: testObjectWithAnnotation(OwningInventoryKey, "unmatched"), inv: &fakeInventoryInfo{id: "random-id"}, policy: AdoptAll, canPrune: true, diff --git a/pkg/kstatus/polling/event/eventtype_string.go b/pkg/kstatus/polling/event/eventtype_string.go index ba39dc75d..fc95f3b42 100644 --- a/pkg/kstatus/polling/event/eventtype_string.go +++ b/pkg/kstatus/polling/event/eventtype_string.go @@ -1,3 +1,6 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + // Code generated by "stringer -type=EventType"; DO NOT EDIT. package event diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 4ce9acdee..a6ba2f11f 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -36,6 +36,16 @@ func withNamespace(obj *unstructured.Unstructured, namespace string) *unstructur return obj } +func withAnnotation(obj *unstructured.Unstructured, key, value string) *unstructured.Unstructured { + annotations := obj.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[key] = value + obj.SetAnnotations(annotations) + return obj +} + func withDependsOn(obj *unstructured.Unstructured, dep string) *unstructured.Unstructured { a := obj.GetAnnotations() if a == nil { diff --git a/test/e2e/deletion_prevention_test.go b/test/e2e/deletion_prevention_test.go new file mode 100644 index 000000000..9d3b303e7 --- /dev/null +++ b/test/e2e/deletion_prevention_test.go @@ -0,0 +1,97 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/apply" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func deletionPreventionTest(c client.Client, invConfig InventoryConfig, inventoryName, namespaceName string) { + By("Apply resources") + applier := invConfig.ApplierFactoryFunc() + inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) + + inventoryInfo := createInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) + + resources := []*unstructured.Unstructured{ + withNamespace(manifestToUnstructured(deployment1), namespaceName), + withAnnotation(withNamespace(manifestToUnstructured(pod1), namespaceName), common.OnRemoveAnnotation, common.OnRemoveKeep), + withAnnotation(withNamespace(manifestToUnstructured(pod2), namespaceName), common.LifecycleDeleteAnnotation, common.PreventDeletion), + } + + runCollect(applier.Run(context.TODO(), inventoryInfo, resources, apply.Options{ + ReconcileTimeout: 2 * time.Minute, + })) + + By("Verify deployment created") + obj := assertUnstructuredExists(c, withNamespace(manifestToUnstructured(deployment1), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify pod1 created") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(pod1), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify pod2 created") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(pod2), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify the inventory size is 3") + invConfig.InvSizeVerifyFunc(c, inventoryName, namespaceName, inventoryID, 3) + + resources = []*unstructured.Unstructured{ + withNamespace(manifestToUnstructured(deployment1), namespaceName), + } + + runCollect(applier.Run(context.TODO(), inventoryInfo, resources, apply.Options{ + ReconcileTimeout: 2 * time.Minute, + DryRunStrategy: common.DryRunClient, + })) + By("Verify deployment still exists and has the config.k8s.io/owning-inventory annotation") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(deployment1), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify pod1 still exits and does not have the config.k8s.io/owning-inventory annotation") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(pod1), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify pod2 still exits and does not have the config.k8s.io/owning-inventory annotation") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(pod2), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify the inventory size is still 3") + invConfig.InvSizeVerifyFunc(c, inventoryName, namespaceName, inventoryID, 3) + + resources = []*unstructured.Unstructured{ + withNamespace(manifestToUnstructured(deployment1), namespaceName), + } + + runCollect(applier.Run(context.TODO(), inventoryInfo, resources, apply.Options{ + ReconcileTimeout: 2 * time.Minute, + })) + + By("Verify deployment still exists and has the config.k8s.io/owning-inventory annotation") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(deployment1), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal(inventoryInfo.ID())) + + By("Verify pod1 still exits and does not have the config.k8s.io/owning-inventory annotation") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(pod1), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal("")) + + By("Verify pod2 still exits and does not have the config.k8s.io/owning-inventory annotation") + obj = assertUnstructuredExists(c, withNamespace(manifestToUnstructured(pod2), namespaceName)) + Expect(obj.GetAnnotations()[inventory.OwningInventoryKey]).To(Equal("")) + + By("Verify the inventory size is 1") + invConfig.InvSizeVerifyFunc(c, inventoryName, namespaceName, inventoryID, 1) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 97c4b382d..68b0df8cb 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -144,6 +144,10 @@ var _ = Describe("Applier", func() { applyAndDestroyTest(c, invConfig, inventoryName, namespace.GetName()) }) + It("Deletion Prevention", func() { + deletionPreventionTest(c, invConfig, inventoryName, namespace.GetName()) + }) + It("Apply CRD and CR", func() { crdTest(c, invConfig, inventoryName, namespace.GetName()) })