mirror of https://github.com/fluxcd/cli-utils.git
Handle deletion prevention correctly
If an object with the deletion prevention annotation is removed from the inventory, the config.k8s.io/owning-inventory annotation should be removed from the object, and the object should be removed from the inventory.
This commit is contained in:
parent
223d4d51b4
commit
7c27474df5
|
|
@ -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.
|
||||
|
||||
<!-- @createFirstCM @testE2EAgainstLatestRelease-->
|
||||
|
|
@ -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
|
||||
|
||||
<!-- @createSecondCM @testE2EAgainstLatestRelease-->
|
||||
```
|
||||
|
|
@ -70,6 +74,24 @@ data:
|
|||
EOF
|
||||
```
|
||||
|
||||
|
||||
This ConfigMap includes the **client.lifecycle.config.k8s.io/deletion** annotation
|
||||
|
||||
<!-- @createSecondCM @testE2EAgainstLatestRelease-->
|
||||
```
|
||||
cat <<EOF >$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.
|
||||
<!-- @runApply @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
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.
|
||||
<!-- @runDestroyPreview @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
|
|
@ -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.
|
||||
<!-- @runDestroy @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
|
|
@ -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.
|
||||
<!-- @runApplyAgain @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
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
|
||||
<!-- @runDeleteManifest @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
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.
|
||||
<!-- @runPreviewForPrune @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
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.
|
||||
<!-- @runApplyToPrune @testE2EAgainstLatestRelease -->
|
||||
```
|
||||
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;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ import (
|
|||
// "prevent remove" annotation.
|
||||
type PreventRemoveFilter struct{}
|
||||
|
||||
const PreventRemoveFilterName = "PreventRemoveFilter"
|
||||
|
||||
// Name returns the preferred name for the filter. Usually
|
||||
// used for logging.
|
||||
func (prf PreventRemoveFilter) Name() string {
|
||||
return "PreventRemoveFilter"
|
||||
return PreventRemoveFilterName
|
||||
}
|
||||
|
||||
// Filter returns true if the passed object should NOT be pruned (deleted)
|
||||
|
|
|
|||
|
|
@ -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 pruneFilter.Name() == filter.PreventRemoveFilterName {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue