mirror of https://github.com/fluxcd/cli-utils.git
				
				
				
			Refactor prune/delete using ValidationFilter interface
This commit is contained in:
		
							parent
							
								
									70b9f67440
								
							
						
					
					
						commit
						4432f51ac3
					
				| 
						 | 
				
			
			@ -16,6 +16,7 @@ import (
 | 
			
		|||
	"k8s.io/klog/v2"
 | 
			
		||||
	"k8s.io/kubectl/pkg/cmd/util"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/event"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/info"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/poller"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/prune"
 | 
			
		||||
| 
						 | 
				
			
			@ -138,8 +139,6 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.InventoryInfo, obje
 | 
			
		|||
		}
 | 
			
		||||
		// Fetch the queue (channel) of tasks that should be executed.
 | 
			
		||||
		klog.V(4).Infoln("applier building task queue...")
 | 
			
		||||
		// TODO(seans): Remove this once Filter interface implemented.
 | 
			
		||||
		a.pruneOptions.LocalNamespaces = localNamespaces(invInfo, object.UnstructuredsToObjMetas(objects))
 | 
			
		||||
		taskBuilder := &solver.TaskQueueBuilder{
 | 
			
		||||
			PruneOptions: a.pruneOptions,
 | 
			
		||||
			Factory:      a.factory,
 | 
			
		||||
| 
						 | 
				
			
			@ -156,11 +155,22 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.InventoryInfo, obje
 | 
			
		|||
			PruneTimeout:           options.PruneTimeout,
 | 
			
		||||
			InventoryPolicy:        options.InventoryPolicy,
 | 
			
		||||
		}
 | 
			
		||||
		// Build list of prune validation filters.
 | 
			
		||||
		pruneFilters := []filter.ValidationFilter{
 | 
			
		||||
			filter.PreventRemoveFilter{},
 | 
			
		||||
			filter.InventoryPolicyFilter{
 | 
			
		||||
				Inv:       invInfo,
 | 
			
		||||
				InvPolicy: options.InventoryPolicy,
 | 
			
		||||
			},
 | 
			
		||||
			filter.LocalNamespacesFilter{
 | 
			
		||||
				LocalNamespaces: localNamespaces(invInfo, object.UnstructuredsToObjMetas(objects)),
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		// Build the task queue by appending tasks in the proper order.
 | 
			
		||||
		taskQueue := taskBuilder.
 | 
			
		||||
			AppendInvAddTask(invInfo, applyObjs).
 | 
			
		||||
			AppendApplyWaitTasks(invInfo, applyObjs, opts).
 | 
			
		||||
			AppendPruneWaitTasks(invInfo, pruneObjs, opts).
 | 
			
		||||
			AppendPruneWaitTasks(pruneObjs, pruneFilters, opts).
 | 
			
		||||
			AppendInvSetTask(invInfo).
 | 
			
		||||
			Build()
 | 
			
		||||
		// Send event to inform the caller about the resources that
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/klog/v2"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/event"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/poller"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/prune"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/solver"
 | 
			
		||||
| 
						 | 
				
			
			@ -118,11 +119,17 @@ func (d *Destroyer) Run(inv inventory.InventoryInfo, option *DestroyerOption) <-
 | 
			
		|||
			PruneTimeout:           option.DeleteTimeout,
 | 
			
		||||
			DryRunStrategy:         option.DryRunStrategy,
 | 
			
		||||
			PrunePropagationPolicy: option.DeletePropagationPolicy,
 | 
			
		||||
			InventoryPolicy:        option.InventoryPolicy,
 | 
			
		||||
		}
 | 
			
		||||
		deleteFilters := []filter.ValidationFilter{
 | 
			
		||||
			filter.PreventRemoveFilter{},
 | 
			
		||||
			filter.InventoryPolicyFilter{
 | 
			
		||||
				Inv:       inv,
 | 
			
		||||
				InvPolicy: option.InventoryPolicy,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		// Build the ordered set of tasks to execute.
 | 
			
		||||
		taskQueue := taskBuilder.
 | 
			
		||||
			AppendPruneWaitTasks(inv, deleteObjs, opts).
 | 
			
		||||
			AppendPruneWaitTasks(deleteObjs, deleteFilters, opts).
 | 
			
		||||
			AppendDeleteInvTask(inv).
 | 
			
		||||
			Build()
 | 
			
		||||
		// Send event to inform the caller about the resources that
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
// Copyright 2021 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CurrentUIDFilter implements ValidationFilter interface to determine
 | 
			
		||||
// if an object should not be pruned (deleted) because it has recently
 | 
			
		||||
// been applied.
 | 
			
		||||
type CurrentUIDFilter struct {
 | 
			
		||||
	CurrentUIDs sets.String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name returns a filter identifier for logging.
 | 
			
		||||
func (cuf CurrentUIDFilter) Name() string {
 | 
			
		||||
	return "CurrentUIDFilter"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Filter returns true if the passed object should NOT be pruned (deleted)
 | 
			
		||||
// because the it is a namespace that objects still reside in; otherwise
 | 
			
		||||
// returns false. This filter should not be added to the list of filters
 | 
			
		||||
// for "destroying", since every object is being deletet. Never returns an error.
 | 
			
		||||
func (cuf CurrentUIDFilter) Filter(obj *unstructured.Unstructured) (bool, error) {
 | 
			
		||||
	uid := string(obj.GetUID())
 | 
			
		||||
	if cuf.CurrentUIDs.Has(uid) {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
// Copyright 2019 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/types"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCurrentUIDFilter(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		filterUIDs sets.String
 | 
			
		||||
		objUID     string
 | 
			
		||||
		filtered   bool
 | 
			
		||||
	}{
 | 
			
		||||
		"Empty filter UIDs, object is not filtered": {
 | 
			
		||||
			filterUIDs: sets.NewString(),
 | 
			
		||||
			objUID:     "bar",
 | 
			
		||||
			filtered:   false,
 | 
			
		||||
		},
 | 
			
		||||
		"Empty object UID, object is not filtered": {
 | 
			
		||||
			filterUIDs: sets.NewString("foo"),
 | 
			
		||||
			objUID:     "",
 | 
			
		||||
			filtered:   false,
 | 
			
		||||
		},
 | 
			
		||||
		"Object UID not in filter UID set, object is not filtered": {
 | 
			
		||||
			filterUIDs: sets.NewString("foo", "baz"),
 | 
			
		||||
			objUID:     "bar",
 | 
			
		||||
			filtered:   false,
 | 
			
		||||
		},
 | 
			
		||||
		"Object UID is in filter UID set, object is filtered": {
 | 
			
		||||
			filterUIDs: sets.NewString("foo"),
 | 
			
		||||
			objUID:     "foo",
 | 
			
		||||
			filtered:   true,
 | 
			
		||||
		},
 | 
			
		||||
		"Object UID is among several filter UIDs, object is filtered": {
 | 
			
		||||
			filterUIDs: sets.NewString("foo", "bar", "baz"),
 | 
			
		||||
			objUID:     "foo",
 | 
			
		||||
			filtered:   true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for name, tc := range tests {
 | 
			
		||||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			filter := CurrentUIDFilter{
 | 
			
		||||
				CurrentUIDs: tc.filterUIDs,
 | 
			
		||||
			}
 | 
			
		||||
			obj := defaultObj.DeepCopy()
 | 
			
		||||
			obj.SetUID(types.UID(tc.objUID))
 | 
			
		||||
			actual, err := filter.Filter(obj)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("CurrentUIDFilter unexpected error (%s)", err)
 | 
			
		||||
			}
 | 
			
		||||
			if tc.filtered != actual {
 | 
			
		||||
				t.Errorf("CurrentUIDFilter expected filter (%t), got (%t)", tc.filtered, actual)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,19 @@
 | 
			
		|||
// Copyright 2021 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ValidationFilter interface decouples apply/prune validation
 | 
			
		||||
// from the concrete structs used for validation. The apply/prune
 | 
			
		||||
// functionality will run validation filters to remove objects
 | 
			
		||||
// which should not be applied or pruned.
 | 
			
		||||
type ValidationFilter interface {
 | 
			
		||||
	// Name returns a filter name (usually for logging).
 | 
			
		||||
	Name() string
 | 
			
		||||
	// Filter returns true if validation fails or an error.
 | 
			
		||||
	Filter(obj *unstructured.Unstructured) (bool, error)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
// Copyright 2021 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/inventory"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// InventoryPolicyFilter implements ValidationFilter interface to determine
 | 
			
		||||
// if an object should be pruned (deleted) because of the InventoryPolicy
 | 
			
		||||
// and if the objects owning inventory identifier matchs the inventory id.
 | 
			
		||||
type InventoryPolicyFilter struct {
 | 
			
		||||
	Inv       inventory.InventoryInfo
 | 
			
		||||
	InvPolicy inventory.InventoryPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name returns a filter identifier for logging.
 | 
			
		||||
func (ipf InventoryPolicyFilter) Name() string {
 | 
			
		||||
	return "InventoryPolictyFilter"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Filter returns true if the passed object should NOT be pruned (deleted)
 | 
			
		||||
// because the "prevent remove" annotation is present; otherwise returns
 | 
			
		||||
// false. Never returns an error.
 | 
			
		||||
func (ipf InventoryPolicyFilter) Filter(obj *unstructured.Unstructured) (bool, error) {
 | 
			
		||||
	// Check the inventory id "match" and the adopt policy to determine
 | 
			
		||||
	// if an object should be pruned (deleted).
 | 
			
		||||
	if !inventory.CanPrune(ipf.Inv, obj, ipf.InvPolicy) {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,101 @@
 | 
			
		|||
// Copyright 2019 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/common"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/inventory"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var inventoryObj = &unstructured.Unstructured{
 | 
			
		||||
	Object: map[string]interface{}{
 | 
			
		||||
		"apiVersion": "v1",
 | 
			
		||||
		"kind":       "ConfigMap",
 | 
			
		||||
		"metadata": map[string]interface{}{
 | 
			
		||||
			"name":      "inventory-name",
 | 
			
		||||
			"namespace": "inventory-namespace",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInventoryPolicyFilter(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		inventoryID    string
 | 
			
		||||
		objInventoryID string
 | 
			
		||||
		policy         inventory.InventoryPolicy
 | 
			
		||||
		filtered       bool
 | 
			
		||||
	}{
 | 
			
		||||
		"inventory and object ids match, not filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "foo",
 | 
			
		||||
			policy:         inventory.InventoryPolicyMustMatch,
 | 
			
		||||
			filtered:       false,
 | 
			
		||||
		},
 | 
			
		||||
		"inventory and object ids match and adopt, not filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "foo",
 | 
			
		||||
			policy:         inventory.AdoptIfNoInventory,
 | 
			
		||||
			filtered:       false,
 | 
			
		||||
		},
 | 
			
		||||
		"inventory and object ids do no match and policy must match, filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "bar",
 | 
			
		||||
			policy:         inventory.InventoryPolicyMustMatch,
 | 
			
		||||
			filtered:       true,
 | 
			
		||||
		},
 | 
			
		||||
		"inventory and object ids do no match and adopt if no inventory, filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "bar",
 | 
			
		||||
			policy:         inventory.AdoptIfNoInventory,
 | 
			
		||||
			filtered:       true,
 | 
			
		||||
		},
 | 
			
		||||
		"inventory and object ids do no match and adopt all, not filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "bar",
 | 
			
		||||
			policy:         inventory.AdoptAll,
 | 
			
		||||
			filtered:       false,
 | 
			
		||||
		},
 | 
			
		||||
		"object id empty and adopt all, not filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "",
 | 
			
		||||
			policy:         inventory.AdoptAll,
 | 
			
		||||
			filtered:       false,
 | 
			
		||||
		},
 | 
			
		||||
		"object id empty and policy must match, filtered": {
 | 
			
		||||
			inventoryID:    "foo",
 | 
			
		||||
			objInventoryID: "",
 | 
			
		||||
			policy:         inventory.InventoryPolicyMustMatch,
 | 
			
		||||
			filtered:       true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for name, tc := range tests {
 | 
			
		||||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			invIDLabel := map[string]string{
 | 
			
		||||
				common.InventoryLabel: tc.inventoryID,
 | 
			
		||||
			}
 | 
			
		||||
			invObj := inventoryObj.DeepCopy()
 | 
			
		||||
			invObj.SetLabels(invIDLabel)
 | 
			
		||||
			filter := InventoryPolicyFilter{
 | 
			
		||||
				Inv:       inventory.WrapInventoryInfoObj(invObj),
 | 
			
		||||
				InvPolicy: tc.policy,
 | 
			
		||||
			}
 | 
			
		||||
			objIDAnnotation := map[string]string{
 | 
			
		||||
				"config.k8s.io/owning-inventory": tc.objInventoryID,
 | 
			
		||||
			}
 | 
			
		||||
			obj := defaultObj.DeepCopy()
 | 
			
		||||
			obj.SetAnnotations(objIDAnnotation)
 | 
			
		||||
			actual, err := filter.Filter(obj)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("InventoryPolicyFilter unexpected error (%s)", err)
 | 
			
		||||
			}
 | 
			
		||||
			if tc.filtered != actual {
 | 
			
		||||
				t.Errorf("InventoryPolicyFilter expected filter (%t), got (%t)", tc.filtered, actual)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
// Copyright 2021 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/object"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// LocalNamespacesFilter encapsulates the set of namespaces
 | 
			
		||||
// that are currently in use. Used to ensure we do not delete
 | 
			
		||||
// namespaces with currently applied objects in them.
 | 
			
		||||
type LocalNamespacesFilter struct {
 | 
			
		||||
	LocalNamespaces sets.String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name returns a filter identifier for logging.
 | 
			
		||||
func (lnf LocalNamespacesFilter) Name() string {
 | 
			
		||||
	return "LocalNamespacesFilter"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Filter returns true if the passed object should NOT be pruned (deleted)
 | 
			
		||||
// because the it is a namespace that objects still reside in; otherwise
 | 
			
		||||
// returns false. This filter should not be added to the list of filters
 | 
			
		||||
// for "destroying", since every object is being delete. Never returns an error.
 | 
			
		||||
func (lnf LocalNamespacesFilter) Filter(obj *unstructured.Unstructured) (bool, error) {
 | 
			
		||||
	id := object.UnstructuredToObjMeta(obj)
 | 
			
		||||
	if id.GroupKind == object.CoreV1Namespace.GroupKind() &&
 | 
			
		||||
		lnf.LocalNamespaces.Has(id.Name) {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
// Copyright 2019 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var testNamespace = &unstructured.Unstructured{
 | 
			
		||||
	Object: map[string]interface{}{
 | 
			
		||||
		"apiVersion": "v1",
 | 
			
		||||
		"kind":       "Namespace",
 | 
			
		||||
		"metadata": map[string]interface{}{
 | 
			
		||||
			"name": "test-namespace",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestLocalNamespacesFilter(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		localNamespaces sets.String
 | 
			
		||||
		namespace       string
 | 
			
		||||
		filtered        bool
 | 
			
		||||
	}{
 | 
			
		||||
		"No local namespaces, namespace is not filtered": {
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			namespace:       "test-namespace",
 | 
			
		||||
			filtered:        false,
 | 
			
		||||
		},
 | 
			
		||||
		"Namespace not in local namespaces, namespace is not filtered": {
 | 
			
		||||
			localNamespaces: sets.NewString("foo", "bar"),
 | 
			
		||||
			namespace:       "test-namespace",
 | 
			
		||||
			filtered:        false,
 | 
			
		||||
		},
 | 
			
		||||
		"Namespace is in local namespaces, namespace is filtered": {
 | 
			
		||||
			localNamespaces: sets.NewString("foo", "test-namespace", "bar"),
 | 
			
		||||
			namespace:       "test-namespace",
 | 
			
		||||
			filtered:        true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for name, tc := range tests {
 | 
			
		||||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			filter := LocalNamespacesFilter{
 | 
			
		||||
				LocalNamespaces: tc.localNamespaces,
 | 
			
		||||
			}
 | 
			
		||||
			namespace := testNamespace.DeepCopy()
 | 
			
		||||
			namespace.SetName(tc.namespace)
 | 
			
		||||
			actual, err := filter.Filter(namespace)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("LocalNamespacesFilter unexpected error (%s)", err)
 | 
			
		||||
			}
 | 
			
		||||
			if tc.filtered != actual {
 | 
			
		||||
				t.Errorf("LocalNamespacesFilter expected filter (%t), got (%t)", tc.filtered, actual)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
// Copyright 2020 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// PreventRemoveFilter implements ValidationFilter interface to determine
 | 
			
		||||
// if an object should not be pruned (deleted) because of a
 | 
			
		||||
// "prevent remove" annotation.
 | 
			
		||||
type PreventRemoveFilter struct{}
 | 
			
		||||
 | 
			
		||||
// Name returns the preferred name for the filter. Usually
 | 
			
		||||
// used for logging.
 | 
			
		||||
func (prf PreventRemoveFilter) Name() string {
 | 
			
		||||
	return "PreventRemoveFilter"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Filter returns true if the passed object should NOT be pruned (deleted)
 | 
			
		||||
// because the "prevent remove" annotation is present; otherwise returns
 | 
			
		||||
// false. Never returns an error.
 | 
			
		||||
func (prf PreventRemoveFilter) Filter(obj *unstructured.Unstructured) (bool, error) {
 | 
			
		||||
	for annotation, value := range obj.GetAnnotations() {
 | 
			
		||||
		if common.NoDeletion(annotation, value) {
 | 
			
		||||
			return true, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
// Copyright 2019 The Kubernetes Authors.
 | 
			
		||||
// SPDX-License-Identifier: Apache-2.0
 | 
			
		||||
 | 
			
		||||
package filter
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/common"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var defaultObj = &unstructured.Unstructured{
 | 
			
		||||
	Object: map[string]interface{}{
 | 
			
		||||
		"apiVersion": "v1",
 | 
			
		||||
		"kind":       "Pod",
 | 
			
		||||
		"metadata": map[string]interface{}{
 | 
			
		||||
			"name":      "pod-name",
 | 
			
		||||
			"namespace": "test-namespace",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPreventDeleteAnnotation(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		annotations map[string]string
 | 
			
		||||
		expected    bool
 | 
			
		||||
	}{
 | 
			
		||||
		"Nil map returns false": {
 | 
			
		||||
			annotations: nil,
 | 
			
		||||
			expected:    false,
 | 
			
		||||
		},
 | 
			
		||||
		"Empty map returns false": {
 | 
			
		||||
			annotations: map[string]string{},
 | 
			
		||||
			expected:    false,
 | 
			
		||||
		},
 | 
			
		||||
		"Wrong annotation key/value is false": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				"foo": "bar",
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key without value is false": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.OnRemoveAnnotation: "bar",
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key and value is true": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.OnRemoveAnnotation: common.OnRemoveKeep,
 | 
			
		||||
			},
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key client.lifecycle.config.k8s.io/deletion without value is false": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.LifecycleDeleteAnnotation: "any",
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key client.lifecycle.config.k8s.io/deletion and value is true": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.LifecycleDeleteAnnotation: common.PreventDeletion,
 | 
			
		||||
			},
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for name, tc := range tests {
 | 
			
		||||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			filter := PreventRemoveFilter{}
 | 
			
		||||
			obj := defaultObj.DeepCopy()
 | 
			
		||||
			obj.SetAnnotations(tc.annotations)
 | 
			
		||||
			actual, err := filter.Filter(obj)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("PreventRemoveFilter unexpected error (%s)", err)
 | 
			
		||||
			}
 | 
			
		||||
			if tc.expected != actual {
 | 
			
		||||
				t.Errorf("PreventRemoveFilter expected (%t), got (%t)", tc.expected, actual)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,16 +13,15 @@ package prune
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/sets"
 | 
			
		||||
	"k8s.io/client-go/dynamic"
 | 
			
		||||
	"k8s.io/klog/v2"
 | 
			
		||||
	"k8s.io/kubectl/pkg/cmd/util"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/common"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/inventory"
 | 
			
		||||
| 
						 | 
				
			
			@ -39,8 +38,6 @@ type PruneOptions struct {
 | 
			
		|||
	// True if we are destroying, which deletes the inventory object
 | 
			
		||||
	// as well (possibly) the inventory namespace.
 | 
			
		||||
	Destroy bool
 | 
			
		||||
	// TODO(seans): Replace this with Filter interface to generalize prune skipping.
 | 
			
		||||
	LocalNamespaces sets.String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPruneOptions returns a struct (PruneOptions) encapsulating the necessary
 | 
			
		||||
| 
						 | 
				
			
			@ -48,8 +45,7 @@ type PruneOptions struct {
 | 
			
		|||
// gathering this information.
 | 
			
		||||
func NewPruneOptions() *PruneOptions {
 | 
			
		||||
	po := &PruneOptions{
 | 
			
		||||
		Destroy:         false,
 | 
			
		||||
		LocalNamespaces: sets.NewString(),
 | 
			
		||||
		Destroy: false,
 | 
			
		||||
	}
 | 
			
		||||
	return po
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -77,55 +73,57 @@ type Options struct {
 | 
			
		|||
	DryRunStrategy common.DryRunStrategy
 | 
			
		||||
 | 
			
		||||
	PropagationPolicy metav1.DeletionPropagation
 | 
			
		||||
 | 
			
		||||
	// InventoryPolicy defines the inventory policy of prune.
 | 
			
		||||
	InventoryPolicy inventory.InventoryPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Prune deletes the set of passed pruneObjs.
 | 
			
		||||
// Prune deletes the set of passed pruneObjs. A prune skip/failure is
 | 
			
		||||
// captured in the TaskContext, so we do not lose track of these
 | 
			
		||||
// objects from the inventory. The passed prune filters are used to
 | 
			
		||||
// determine if permission exists to delete the object. An example
 | 
			
		||||
// of a prune filter is PreventDeleteFilter, which checks if an
 | 
			
		||||
// annotation exists on the object to ensure the objects is not
 | 
			
		||||
// deleted (e.g. a PersistentVolume that we do no want to
 | 
			
		||||
// automatically prune/delete).
 | 
			
		||||
//
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   localInv - locally read inventory object
 | 
			
		||||
//   pruneObjs - objects to prune (delete)
 | 
			
		||||
//   currentUIDs - UIDs for successfully applied objects
 | 
			
		||||
//   pruneFilters - list of filters for deletion permission
 | 
			
		||||
//   taskContext - task for apply/prune
 | 
			
		||||
func (po *PruneOptions) Prune(localInv inventory.InventoryInfo,
 | 
			
		||||
	pruneObjs []*unstructured.Unstructured,
 | 
			
		||||
	currentUIDs sets.String,
 | 
			
		||||
//   o - options for dry-run
 | 
			
		||||
func (po *PruneOptions) Prune(pruneObjs []*unstructured.Unstructured,
 | 
			
		||||
	pruneFilters []filter.ValidationFilter,
 | 
			
		||||
	taskContext *taskrunner.TaskContext,
 | 
			
		||||
	o Options) error {
 | 
			
		||||
	// Validate parameters
 | 
			
		||||
	if localInv == nil {
 | 
			
		||||
		return fmt.Errorf("the local inventory object can't be nil")
 | 
			
		||||
	}
 | 
			
		||||
	// Sort the resources in reverse order using the same rules as is
 | 
			
		||||
	// used for apply.
 | 
			
		||||
	eventFactory := CreateEventFactory(po.Destroy)
 | 
			
		||||
	// Iterate through objects to prune (delete). If an object is not pruned
 | 
			
		||||
	// and we need to keep it in the inventory, we must capture the prune failure.
 | 
			
		||||
	for _, pruneObj := range pruneObjs {
 | 
			
		||||
		pruneID := object.UnstructuredToObjMeta(pruneObj)
 | 
			
		||||
		klog.V(5).Infof("attempting prune: %s", pruneID)
 | 
			
		||||
		// Do not prune objects that are in set of currently applied objects.
 | 
			
		||||
		uid := string(pruneObj.GetUID())
 | 
			
		||||
		if currentUIDs.Has(uid) {
 | 
			
		||||
			klog.V(5).Infof("prune object in current apply; do not prune: %s", uid)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// Handle lifecycle directive preventing deletion.
 | 
			
		||||
		if !canPrune(localInv, pruneObj, o.InventoryPolicy, uid) {
 | 
			
		||||
			klog.V(4).Infof("skip prune for lifecycle directive %s", pruneID)
 | 
			
		||||
			taskContext.EventChannel() <- eventFactory.CreateSkippedEvent(pruneObj)
 | 
			
		||||
			taskContext.CapturePruneFailure(pruneID)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// If regular pruning (not destroying), skip deleting namespace containing
 | 
			
		||||
		// currently applied objects.
 | 
			
		||||
		if pruneID.GroupKind == object.CoreV1Namespace.GroupKind() &&
 | 
			
		||||
			po.LocalNamespaces.Has(pruneID.Name) {
 | 
			
		||||
			klog.V(4).Infof("skip pruning namespace: %s", pruneID.Name)
 | 
			
		||||
			taskContext.EventChannel() <- eventFactory.CreateSkippedEvent(pruneObj)
 | 
			
		||||
			taskContext.CapturePruneFailure(pruneID)
 | 
			
		||||
		// Check filters to see if we're prevented from pruning/deleting object.
 | 
			
		||||
		var filtered bool
 | 
			
		||||
		var err error
 | 
			
		||||
		for _, filter := range pruneFilters {
 | 
			
		||||
			klog.V(6).Infof("prune filter %s: %s", filter.Name(), pruneID)
 | 
			
		||||
			filtered, err = filter.Filter(pruneObj)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if klog.V(5).Enabled() {
 | 
			
		||||
					klog.Errorf("error during %s, (%s): %s", filter.Name(), pruneID, err)
 | 
			
		||||
				}
 | 
			
		||||
				taskContext.EventChannel() <- eventFactory.CreateFailedEvent(pruneID, err)
 | 
			
		||||
				taskContext.CapturePruneFailure(pruneID)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			if filtered {
 | 
			
		||||
				klog.V(4).Infof("prune filtered by %s: %s", filter.Name(), pruneID)
 | 
			
		||||
				taskContext.EventChannel() <- eventFactory.CreateSkippedEvent(pruneObj)
 | 
			
		||||
				taskContext.CapturePruneFailure(pruneID)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if filtered || err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		// Filters passed--actually delete object if not dry run.
 | 
			
		||||
		if !o.DryRunStrategy.ClientOrServerDryRun() {
 | 
			
		||||
			klog.V(4).Infof("prune object delete: %s", pruneID)
 | 
			
		||||
			namespacedClient, err := po.namespacedClient(pruneID)
 | 
			
		||||
| 
						 | 
				
			
			@ -193,27 +191,3 @@ func (po *PruneOptions) namespacedClient(obj object.ObjMetadata) (dynamic.Resour
 | 
			
		|||
	}
 | 
			
		||||
	return po.Client.Resource(mapping.Resource).Namespace(obj.Namespace), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// preventDeleteAnnotation returns true if the "onRemove:keep"
 | 
			
		||||
// annotation exists within the annotation map; false otherwise.
 | 
			
		||||
func preventDeleteAnnotation(annotations map[string]string) bool {
 | 
			
		||||
	for annotation, value := range annotations {
 | 
			
		||||
		if common.NoDeletion(annotation, value) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func canPrune(localInv inventory.InventoryInfo, obj *unstructured.Unstructured,
 | 
			
		||||
	policy inventory.InventoryPolicy, uid string) bool {
 | 
			
		||||
	if !inventory.CanPrune(localInv, obj, policy) {
 | 
			
		||||
		klog.V(4).Infof("skip pruning object that doesn't belong to current inventory: %s", uid)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if preventDeleteAnnotation(obj.GetAnnotations()) {
 | 
			
		||||
		klog.V(4).Infof("prune object lifecycle directive; do not prune: %s", uid)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import (
 | 
			
		|||
	"k8s.io/client-go/dynamic/fake"
 | 
			
		||||
	"k8s.io/kubectl/pkg/scheme"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/event"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/common"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/inventory"
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +71,7 @@ var pod = &unstructured.Unstructured{
 | 
			
		|||
		"metadata": map[string]interface{}{
 | 
			
		||||
			"name":      podName,
 | 
			
		||||
			"namespace": testNamespace,
 | 
			
		||||
			"uid":       "uid1",
 | 
			
		||||
			"uid":       "pod-uid",
 | 
			
		||||
			"annotations": map[string]interface{}{
 | 
			
		||||
				"config.k8s.io/owning-inventory": testInventoryLabel,
 | 
			
		||||
			},
 | 
			
		||||
| 
						 | 
				
			
			@ -160,39 +161,33 @@ var (
 | 
			
		|||
	defaultOptions = Options{
 | 
			
		||||
		DryRunStrategy:    common.DryRunNone,
 | 
			
		||||
		PropagationPolicy: metav1.DeletePropagationBackground,
 | 
			
		||||
		InventoryPolicy:   inventory.InventoryPolicyMustMatch,
 | 
			
		||||
	}
 | 
			
		||||
	clientDryRunOptions = Options{
 | 
			
		||||
		DryRunStrategy:    common.DryRunClient,
 | 
			
		||||
		PropagationPolicy: metav1.DeletePropagationBackground,
 | 
			
		||||
		InventoryPolicy:   inventory.InventoryPolicyMustMatch,
 | 
			
		||||
	}
 | 
			
		||||
	serverDryRunOptions = Options{
 | 
			
		||||
		DryRunStrategy:    common.DryRunServer,
 | 
			
		||||
		PropagationPolicy: metav1.DeletePropagationBackground,
 | 
			
		||||
		InventoryPolicy:   inventory.InventoryPolicyMustMatch,
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPrune(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		pruneObjs       []*unstructured.Unstructured
 | 
			
		||||
		destroy         bool
 | 
			
		||||
		options         Options
 | 
			
		||||
		currentUIDs     sets.String
 | 
			
		||||
		localNamespaces sets.String
 | 
			
		||||
		expectedEvents  []testutil.ExpEvent
 | 
			
		||||
		pruneObjs      []*unstructured.Unstructured
 | 
			
		||||
		pruneFilters   []filter.ValidationFilter
 | 
			
		||||
		destroy        bool
 | 
			
		||||
		options        Options
 | 
			
		||||
		expectedEvents []testutil.ExpEvent
 | 
			
		||||
	}{
 | 
			
		||||
		"No pruned objects; no prune/delete events": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			expectedEvents:  []testutil.ExpEvent{},
 | 
			
		||||
			pruneObjs:      []*unstructured.Unstructured{},
 | 
			
		||||
			options:        defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{},
 | 
			
		||||
		},
 | 
			
		||||
		"One successfully pruned object": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod},
 | 
			
		||||
			options:   defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
| 
						 | 
				
			
			@ -203,9 +198,8 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Multiple successfully pruned object": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod, pdb, namespace},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod, pdb, namespace},
 | 
			
		||||
			options:   defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
| 
						 | 
				
			
			@ -228,10 +222,9 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"One successfully deleted object": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod},
 | 
			
		||||
			destroy:         true,
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod},
 | 
			
		||||
			destroy:   true,
 | 
			
		||||
			options:   defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.DeleteType,
 | 
			
		||||
| 
						 | 
				
			
			@ -242,10 +235,9 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Multiple successfully deleted objects": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod, pdb, namespace},
 | 
			
		||||
			destroy:         true,
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod, pdb, namespace},
 | 
			
		||||
			destroy:   true,
 | 
			
		||||
			options:   defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.DeleteType,
 | 
			
		||||
| 
						 | 
				
			
			@ -268,9 +260,8 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Client dry run still pruned event": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod},
 | 
			
		||||
			options:         clientDryRunOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod},
 | 
			
		||||
			options:   clientDryRunOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
| 
						 | 
				
			
			@ -281,10 +272,9 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Server dry run still deleted event": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod},
 | 
			
		||||
			destroy:         true,
 | 
			
		||||
			options:         serverDryRunOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod},
 | 
			
		||||
			destroy:   true,
 | 
			
		||||
			options:   serverDryRunOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.DeleteType,
 | 
			
		||||
| 
						 | 
				
			
			@ -294,19 +284,40 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"UID match means no prune": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			currentUIDs:     sets.NewString("uid1"),
 | 
			
		||||
			expectedEvents:  []testutil.ExpEvent{},
 | 
			
		||||
		},
 | 
			
		||||
		"UID match for only one object means only one pruned": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{pod, pdb},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			currentUIDs:     sets.NewString("uid1"),
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
		"UID match means prune skipped": {
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod},
 | 
			
		||||
			pruneFilters: []filter.ValidationFilter{
 | 
			
		||||
				filter.CurrentUIDFilter{
 | 
			
		||||
					// Add pod UID to set of current UIDs
 | 
			
		||||
					CurrentUIDs: sets.NewString("pod-uid"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			options: defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
					PruneEvent: &testutil.ExpPruneEvent{
 | 
			
		||||
						Operation: event.PruneSkipped,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"UID match for only one object one pruned, one skipped": {
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{pod, pdb},
 | 
			
		||||
			pruneFilters: []filter.ValidationFilter{
 | 
			
		||||
				filter.CurrentUIDFilter{
 | 
			
		||||
					// Add pod UID to set of current UIDs
 | 
			
		||||
					CurrentUIDs: sets.NewString("pod-uid"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			options: defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
					PruneEvent: &testutil.ExpPruneEvent{
 | 
			
		||||
						Operation: event.PruneSkipped,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
					PruneEvent: &testutil.ExpPruneEvent{
 | 
			
		||||
| 
						 | 
				
			
			@ -316,9 +327,9 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Prevent delete annotation equals prune skipped": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{preventDelete},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs:    []*unstructured.Unstructured{preventDelete},
 | 
			
		||||
			pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}},
 | 
			
		||||
			options:      defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
| 
						 | 
				
			
			@ -329,10 +340,10 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Prevent delete annotation equals delete skipped": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{preventDelete},
 | 
			
		||||
			destroy:         true,
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs:    []*unstructured.Unstructured{preventDelete},
 | 
			
		||||
			pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}},
 | 
			
		||||
			destroy:      true,
 | 
			
		||||
			options:      defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.DeleteType,
 | 
			
		||||
| 
						 | 
				
			
			@ -343,9 +354,9 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Prevent delete annotation, one skipped, one pruned": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{preventDelete, pod},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(),
 | 
			
		||||
			pruneObjs:    []*unstructured.Unstructured{preventDelete, pod},
 | 
			
		||||
			pruneFilters: []filter.ValidationFilter{filter.PreventRemoveFilter{}},
 | 
			
		||||
			options:      defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
| 
						 | 
				
			
			@ -362,9 +373,13 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
			},
 | 
			
		||||
		},
 | 
			
		||||
		"Namespace prune skipped": {
 | 
			
		||||
			pruneObjs:       []*unstructured.Unstructured{namespace},
 | 
			
		||||
			options:         defaultOptions,
 | 
			
		||||
			localNamespaces: sets.NewString(namespace.GetName()),
 | 
			
		||||
			pruneObjs: []*unstructured.Unstructured{namespace},
 | 
			
		||||
			pruneFilters: []filter.ValidationFilter{
 | 
			
		||||
				filter.LocalNamespacesFilter{
 | 
			
		||||
					LocalNamespaces: sets.NewString(namespace.GetName()),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			options: defaultOptions,
 | 
			
		||||
			expectedEvents: []testutil.ExpEvent{
 | 
			
		||||
				{
 | 
			
		||||
					EventType: event.PruneType,
 | 
			
		||||
| 
						 | 
				
			
			@ -380,11 +395,9 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			po := NewPruneOptions()
 | 
			
		||||
			po.Destroy = tc.destroy
 | 
			
		||||
			po.LocalNamespaces = tc.localNamespaces
 | 
			
		||||
			pruneIds := object.UnstructuredsToObjMetas(tc.pruneObjs)
 | 
			
		||||
			fakeInvClient := inventory.NewFakeInventoryClient(pruneIds)
 | 
			
		||||
			po.InvClient = fakeInvClient
 | 
			
		||||
			currentInventory := createInventoryInfo(tc.pruneObjs...)
 | 
			
		||||
			// Set up the fake dynamic client to recognize all objects, and the RESTMapper.
 | 
			
		||||
			objs := []runtime.Object{}
 | 
			
		||||
			for _, obj := range tc.pruneObjs {
 | 
			
		||||
| 
						 | 
				
			
			@ -395,12 +408,12 @@ func TestPrune(t *testing.T) {
 | 
			
		|||
				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, len(tc.pruneObjs))
 | 
			
		||||
			eventChannel := make(chan event.Event, len(tc.pruneObjs)+1)
 | 
			
		||||
			taskContext := taskrunner.NewTaskContext(eventChannel)
 | 
			
		||||
			err := func() error {
 | 
			
		||||
				defer close(eventChannel)
 | 
			
		||||
				// Run the prune and validate.
 | 
			
		||||
				return po.Prune(currentInventory, tc.pruneObjs, tc.currentUIDs, taskContext, tc.options)
 | 
			
		||||
				return po.Prune(tc.pruneObjs, tc.pruneFilters, taskContext, tc.options)
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -456,7 +469,6 @@ func TestPruneWithErrors(t *testing.T) {
 | 
			
		|||
			pruneIds := object.UnstructuredsToObjMetas(tc.pruneObjs)
 | 
			
		||||
			fakeInvClient := inventory.NewFakeInventoryClient(pruneIds)
 | 
			
		||||
			po.InvClient = fakeInvClient
 | 
			
		||||
			currentInventory := createInventoryInfo(tc.pruneObjs...)
 | 
			
		||||
			// Set up the fake dynamic client to recognize all objects, and the RESTMapper.
 | 
			
		||||
			po.Client = &fakeDynamicFailureClient{dynamic: fake.NewSimpleDynamicClient(scheme.Scheme,
 | 
			
		||||
				namespace, pdb, role)}
 | 
			
		||||
| 
						 | 
				
			
			@ -469,7 +481,7 @@ func TestPruneWithErrors(t *testing.T) {
 | 
			
		|||
			err := func() error {
 | 
			
		||||
				defer close(eventChannel)
 | 
			
		||||
				// Run the prune and validate.
 | 
			
		||||
				return po.Prune(currentInventory, tc.pruneObjs, sets.NewString(), taskContext, defaultOptions)
 | 
			
		||||
				return po.Prune(tc.pruneObjs, []filter.ValidationFilter{}, taskContext, defaultOptions)
 | 
			
		||||
			}()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("Unexpected error during Prune(): %#v", err)
 | 
			
		||||
| 
						 | 
				
			
			@ -554,60 +566,6 @@ func TestGetPruneObjs(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPreventDeleteAnnotation(t *testing.T) {
 | 
			
		||||
	tests := map[string]struct {
 | 
			
		||||
		annotations map[string]string
 | 
			
		||||
		expected    bool
 | 
			
		||||
	}{
 | 
			
		||||
		"Nil map returns false": {
 | 
			
		||||
			annotations: nil,
 | 
			
		||||
			expected:    false,
 | 
			
		||||
		},
 | 
			
		||||
		"Empty map returns false": {
 | 
			
		||||
			annotations: map[string]string{},
 | 
			
		||||
			expected:    false,
 | 
			
		||||
		},
 | 
			
		||||
		"Wrong annotation key/value is false": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				"foo": "bar",
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key without value is false": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.OnRemoveAnnotation: "bar",
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key and value is true": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.OnRemoveAnnotation: common.OnRemoveKeep,
 | 
			
		||||
			},
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key client.lifecycle.config.k8s.io/deletion without value is false": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.LifecycleDeleteAnnotation: "any",
 | 
			
		||||
			},
 | 
			
		||||
			expected: false,
 | 
			
		||||
		},
 | 
			
		||||
		"Annotation key client.lifecycle.config.k8s.io/deletion and value is true": {
 | 
			
		||||
			annotations: map[string]string{
 | 
			
		||||
				common.LifecycleDeleteAnnotation: common.PreventDeletion,
 | 
			
		||||
			},
 | 
			
		||||
			expected: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for name, tc := range tests {
 | 
			
		||||
		t.Run(name, func(t *testing.T) {
 | 
			
		||||
			actual := preventDeleteAnnotation(tc.annotations)
 | 
			
		||||
			if tc.expected != actual {
 | 
			
		||||
				t.Errorf("preventDeleteAnnotation Expected (%t), got (%t)", tc.expected, actual)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type fakeDynamicFailureClient struct {
 | 
			
		||||
	dynamic dynamic.Interface
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,7 @@ import (
 | 
			
		|||
	"k8s.io/klog/v2"
 | 
			
		||||
	"k8s.io/kubectl/pkg/cmd/util"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/event"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/info"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/prune"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/task"
 | 
			
		||||
| 
						 | 
				
			
			@ -180,17 +181,17 @@ func (t *TaskQueueBuilder) AppendWaitTask(waitIds []object.ObjMetadata) *TaskQue
 | 
			
		|||
 | 
			
		||||
// AppendInvAddTask appends a task to delete objects from the cluster to the task queue.
 | 
			
		||||
// Returns a pointer to the Builder to chain function calls.
 | 
			
		||||
func (t *TaskQueueBuilder) AppendPruneTask(inv inventory.InventoryInfo, pruneObjs []*unstructured.Unstructured, o Options) *TaskQueueBuilder {
 | 
			
		||||
func (t *TaskQueueBuilder) AppendPruneTask(pruneObjs []*unstructured.Unstructured,
 | 
			
		||||
	pruneFilters []filter.ValidationFilter, o Options) *TaskQueueBuilder {
 | 
			
		||||
	klog.V(5).Infoln("adding prune task")
 | 
			
		||||
	t.tasks = append(t.tasks,
 | 
			
		||||
		&task.PruneTask{
 | 
			
		||||
			TaskName:          fmt.Sprintf("prune-%d", t.pruneCounter),
 | 
			
		||||
			Objects:           pruneObjs,
 | 
			
		||||
			InventoryObject:   inv,
 | 
			
		||||
			Filters:           pruneFilters,
 | 
			
		||||
			PruneOptions:      t.PruneOptions,
 | 
			
		||||
			PropagationPolicy: o.PrunePropagationPolicy,
 | 
			
		||||
			DryRunStrategy:    o.DryRunStrategy,
 | 
			
		||||
			InventoryPolicy:   o.InventoryPolicy,
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	t.pruneCounter += 1
 | 
			
		||||
| 
						 | 
				
			
			@ -221,9 +222,10 @@ func (t *TaskQueueBuilder) AppendApplyWaitTasks(inv inventory.InventoryInfo, app
 | 
			
		|||
// AppendPruneWaitTasks adds prune and wait tasks to the task queue
 | 
			
		||||
// based on build variables (like dry-run). Returns a pointer to the
 | 
			
		||||
// Builder to chain function calls.
 | 
			
		||||
func (t *TaskQueueBuilder) AppendPruneWaitTasks(inv inventory.InventoryInfo, pruneObjs []*unstructured.Unstructured, o Options) *TaskQueueBuilder {
 | 
			
		||||
func (t *TaskQueueBuilder) AppendPruneWaitTasks(pruneObjs []*unstructured.Unstructured,
 | 
			
		||||
	pruneFilters []filter.ValidationFilter, o Options) *TaskQueueBuilder {
 | 
			
		||||
	if o.Prune {
 | 
			
		||||
		t.AppendPruneTask(inv, pruneObjs, o)
 | 
			
		||||
		t.AppendPruneTask(pruneObjs, pruneFilters, o)
 | 
			
		||||
		if !o.DryRunStrategy.ClientOrServerDryRun() && o.PruneTimeout != time.Duration(0) {
 | 
			
		||||
			pruneIds := object.UnstructuredsToObjMetas(pruneObjs)
 | 
			
		||||
			t.AppendWaitTask(pruneIds)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
	"gotest.tools/assert"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"k8s.io/cli-runtime/pkg/resource"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/prune"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/task"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
 | 
			
		||||
| 
						 | 
				
			
			@ -267,7 +268,7 @@ func TestTaskQueueBuilder_BuildTaskQueue(t *testing.T) {
 | 
			
		|||
			tq := tqb.
 | 
			
		||||
				AppendInvAddTask(localInv, tc.objs).
 | 
			
		||||
				AppendApplyWaitTasks(localInv, tc.objs, tc.options).
 | 
			
		||||
				AppendPruneWaitTasks(localInv, emptyPruneObjs, tc.options).
 | 
			
		||||
				AppendPruneWaitTasks(emptyPruneObjs, []filter.ValidationFilter{}, tc.options).
 | 
			
		||||
				AppendInvSetTask(localInv).
 | 
			
		||||
				Build()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,10 @@ import (
 | 
			
		|||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/event"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/filter"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/prune"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/common"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/inventory"
 | 
			
		||||
	"sigs.k8s.io/cli-utils/pkg/object"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,11 +21,10 @@ type PruneTask struct {
 | 
			
		|||
	TaskName string
 | 
			
		||||
 | 
			
		||||
	PruneOptions      *prune.PruneOptions
 | 
			
		||||
	InventoryObject   inventory.InventoryInfo
 | 
			
		||||
	Objects           []*unstructured.Unstructured
 | 
			
		||||
	Filters           []filter.ValidationFilter
 | 
			
		||||
	DryRunStrategy    common.DryRunStrategy
 | 
			
		||||
	PropagationPolicy metav1.DeletionPropagation
 | 
			
		||||
	InventoryPolicy   inventory.InventoryPolicy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *PruneTask) Name() string {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,12 +49,16 @@ func (p *PruneTask) Identifiers() []object.ObjMetadata {
 | 
			
		|||
// to signal to the taskrunner that the task has completed (or failed).
 | 
			
		||||
func (p *PruneTask) Start(taskContext *taskrunner.TaskContext) {
 | 
			
		||||
	go func() {
 | 
			
		||||
		currentUIDs := taskContext.AllResourceUIDs()
 | 
			
		||||
		err := p.PruneOptions.Prune(p.InventoryObject, p.Objects,
 | 
			
		||||
			currentUIDs, taskContext, prune.Options{
 | 
			
		||||
		// Create filter to prevent deletion of currently applied
 | 
			
		||||
		// objects. Must be done here to wait for applied UIDs.
 | 
			
		||||
		uidFilter := filter.CurrentUIDFilter{
 | 
			
		||||
			CurrentUIDs: taskContext.AllResourceUIDs(),
 | 
			
		||||
		}
 | 
			
		||||
		p.Filters = append(p.Filters, uidFilter)
 | 
			
		||||
		err := p.PruneOptions.Prune(p.Objects,
 | 
			
		||||
			p.Filters, taskContext, prune.Options{
 | 
			
		||||
				DryRunStrategy:    p.DryRunStrategy,
 | 
			
		||||
				PropagationPolicy: p.PropagationPolicy,
 | 
			
		||||
				InventoryPolicy:   p.InventoryPolicy,
 | 
			
		||||
			})
 | 
			
		||||
		taskContext.TaskChannel() <- taskrunner.TaskResult{
 | 
			
		||||
			Err: err,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue