...

Source file src/sigs.k8s.io/cli-utils/pkg/apply/prune/prune_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/apply/prune

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

View as plain text