Refactor prune/delete using ValidationFilter interface

This commit is contained in:
Sean Sullivan 2021-06-14 23:35:55 -07:00
parent 70b9f67440
commit 4432f51ac3
16 changed files with 619 additions and 203 deletions

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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()

View File

@ -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,