...

Source file src/sigs.k8s.io/cli-utils/pkg/apply/prune/prune.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  // Prune functionality deletes previously applied objects
     5  // which are subsequently omitted in further apply operations.
     6  // This functionality relies on "inventory" objects to store
     7  // object metadata for each apply operation. This file defines
     8  // PruneOptions to encapsulate information necessary to
     9  // calculate the prune set, and to delete the objects in
    10  // this prune set.
    11  
    12  package prune
    13  
    14  import (
    15  	"context"
    16  	"errors"
    17  
    18  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    19  	"k8s.io/apimachinery/pkg/api/meta"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	"k8s.io/client-go/dynamic"
    23  	"k8s.io/klog/v2"
    24  	"k8s.io/kubectl/pkg/cmd/util"
    25  	"sigs.k8s.io/cli-utils/pkg/apply/filter"
    26  	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
    27  	"sigs.k8s.io/cli-utils/pkg/common"
    28  	"sigs.k8s.io/cli-utils/pkg/inventory"
    29  	"sigs.k8s.io/cli-utils/pkg/object"
    30  )
    31  
    32  // Pruner implements GetPruneObjs to calculate which objects to prune and Prune
    33  // to delete them.
    34  type Pruner struct {
    35  	InvClient inventory.Client
    36  	Client    dynamic.Interface
    37  	Mapper    meta.RESTMapper
    38  }
    39  
    40  // NewPruner returns a new Pruner.
    41  // Returns an error if dependency injection fails using the factory.
    42  func NewPruner(factory util.Factory, invClient inventory.Client) (*Pruner, error) {
    43  	// Client/Builder fields from the Factory.
    44  	client, err := factory.DynamicClient()
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	mapper, err := factory.ToRESTMapper()
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	return &Pruner{
    53  		InvClient: invClient,
    54  		Client:    client,
    55  		Mapper:    mapper,
    56  	}, nil
    57  }
    58  
    59  // Options defines a set of parameters that can be used to tune
    60  // the behavior of the pruner.
    61  type Options struct {
    62  	// DryRunStrategy defines whether objects should actually be pruned or if
    63  	// we should just print what would happen without actually doing it.
    64  	DryRunStrategy common.DryRunStrategy
    65  
    66  	PropagationPolicy metav1.DeletionPropagation
    67  
    68  	// True if we are destroying, which deletes the inventory object
    69  	// as well (possibly) the inventory namespace.
    70  	Destroy bool
    71  }
    72  
    73  // Prune deletes the set of passed objects. A prune skip/failure is
    74  // captured in the TaskContext, so we do not lose track of these
    75  // objects from the inventory. The passed prune filters are used to
    76  // determine if permission exists to delete the object. An example
    77  // of a prune filter is PreventDeleteFilter, which checks if an
    78  // annotation exists on the object to ensure the objects is not
    79  // deleted (e.g. a PersistentVolume that we do no want to
    80  // automatically prune/delete).
    81  //
    82  // Parameters:
    83  //
    84  //	objs - objects to prune (delete)
    85  //	pruneFilters - list of filters for deletion permission
    86  //	taskContext - task for apply/prune
    87  //	taskName - name of the parent task group, for events
    88  //	opts - options for dry-run
    89  func (p *Pruner) Prune(
    90  	objs object.UnstructuredSet,
    91  	pruneFilters []filter.ValidationFilter,
    92  	taskContext *taskrunner.TaskContext,
    93  	taskName string,
    94  	opts Options,
    95  ) error {
    96  	eventFactory := CreateEventFactory(opts.Destroy, taskName)
    97  	// Iterate through objects to prune (delete). If an object is not pruned
    98  	// and we need to keep it in the inventory, we must capture the prune failure.
    99  	for _, obj := range objs {
   100  		id := object.UnstructuredToObjMetadata(obj)
   101  		klog.V(5).Infof("evaluating prune filters (object: %q)", id)
   102  
   103  		// UID will change if the object is deleted and re-created.
   104  		uid := obj.GetUID()
   105  		if uid == "" {
   106  			err := object.NotFound([]interface{}{"metadata", "uid"}, "")
   107  			if klog.V(4).Enabled() {
   108  				// only log event emitted errors if the verbosity > 4
   109  				klog.Errorf("prune uid lookup errored (object: %s): %v", id, err)
   110  			}
   111  			taskContext.SendEvent(eventFactory.CreateFailedEvent(id, err))
   112  			taskContext.InventoryManager().AddFailedDelete(id)
   113  			continue
   114  		}
   115  
   116  		// Check filters to see if we're prevented from pruning/deleting object.
   117  		var filterErr error
   118  		for _, pruneFilter := range pruneFilters {
   119  			klog.V(6).Infof("prune filter evaluating (filter: %s, object: %s)", pruneFilter.Name(), id)
   120  			filterErr = pruneFilter.Filter(obj)
   121  			if filterErr != nil {
   122  				var fatalErr *filter.FatalError
   123  				if errors.As(filterErr, &fatalErr) {
   124  					if klog.V(4).Enabled() {
   125  						// only log event emitted errors if the verbosity > 4
   126  						klog.Errorf("prune filter errored (filter: %s, object: %s): %v", pruneFilter.Name(), id, fatalErr.Err)
   127  					}
   128  					taskContext.SendEvent(eventFactory.CreateFailedEvent(id, fatalErr.Err))
   129  					taskContext.InventoryManager().AddFailedDelete(id)
   130  					break
   131  				}
   132  				klog.V(4).Infof("prune filtered (filter: %s, object: %s): %v", pruneFilter.Name(), id, filterErr)
   133  
   134  				// Remove the inventory annotation if deletion was prevented.
   135  				// This abandons the object so it won't be pruned by future applier runs.
   136  				var abandonErr *filter.AnnotationPreventedDeletionError
   137  				if errors.As(filterErr, &abandonErr) {
   138  					if !opts.DryRunStrategy.ClientOrServerDryRun() {
   139  						var err error
   140  						obj, err = p.removeInventoryAnnotation(obj)
   141  						if err != nil {
   142  							if klog.V(4).Enabled() {
   143  								// only log event emitted errors if the verbosity > 4
   144  								klog.Errorf("error removing annotation (object: %q, annotation: %q): %v", id, inventory.OwningInventoryKey, err)
   145  							}
   146  							taskContext.SendEvent(eventFactory.CreateFailedEvent(id, err))
   147  							taskContext.InventoryManager().AddFailedDelete(id)
   148  							break
   149  						}
   150  						// Inventory annotation was successfully removed from the object.
   151  						// Register for removal from the inventory.
   152  						taskContext.AddAbandonedObject(id)
   153  					}
   154  				}
   155  
   156  				taskContext.SendEvent(eventFactory.CreateSkippedEvent(obj, filterErr))
   157  				taskContext.InventoryManager().AddSkippedDelete(id)
   158  				break
   159  			}
   160  		}
   161  		if filterErr != nil {
   162  			continue
   163  		}
   164  
   165  		// Filters passed--actually delete object if not dry run.
   166  		if !opts.DryRunStrategy.ClientOrServerDryRun() {
   167  			klog.V(4).Infof("deleting object (object: %q)", id)
   168  			err := p.deleteObject(id, metav1.DeleteOptions{
   169  				// Only delete the resource if it hasn't already been deleted
   170  				// and recreated since the last GET. Otherwise error.
   171  				Preconditions: &metav1.Preconditions{
   172  					UID: &uid,
   173  				},
   174  				PropagationPolicy: &opts.PropagationPolicy,
   175  			})
   176  			if err != nil {
   177  				if apierrors.IsNotFound(err) {
   178  					klog.Warningf("error deleting object (object: %q): object not found: object may have been deleted asynchronously by another client", id)
   179  					// treat this as successful idempotent deletion
   180  				} else {
   181  					if klog.V(4).Enabled() {
   182  						// only log event emitted errors if the verbosity > 4
   183  						klog.Errorf("error deleting object (object: %q): %v", id, err)
   184  					}
   185  					taskContext.SendEvent(eventFactory.CreateFailedEvent(id, err))
   186  					taskContext.InventoryManager().AddFailedDelete(id)
   187  					continue
   188  				}
   189  			}
   190  		}
   191  		taskContext.InventoryManager().AddSuccessfulDelete(id, obj.GetUID())
   192  		taskContext.SendEvent(eventFactory.CreateSuccessEvent(obj))
   193  	}
   194  	return nil
   195  }
   196  
   197  // removeInventoryAnnotation removes the `config.k8s.io/owning-inventory` annotation from pruneObj.
   198  func (p *Pruner) removeInventoryAnnotation(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
   199  	// Make a copy of the input object to avoid modifying the input.
   200  	// This prevents race conditions when writing to the underlying map.
   201  	obj = obj.DeepCopy()
   202  	id := object.UnstructuredToObjMetadata(obj)
   203  	annotations := obj.GetAnnotations()
   204  	if annotations != nil {
   205  		if _, ok := annotations[inventory.OwningInventoryKey]; ok {
   206  			klog.V(4).Infof("removing annotation (object: %q, annotation: %q)", id, inventory.OwningInventoryKey)
   207  			delete(annotations, inventory.OwningInventoryKey)
   208  			obj.SetAnnotations(annotations)
   209  			namespacedClient, err := p.namespacedClient(id)
   210  			if err != nil {
   211  				return obj, err
   212  			}
   213  			_, err = namespacedClient.Update(context.TODO(), obj, metav1.UpdateOptions{})
   214  			return obj, err
   215  		}
   216  	}
   217  	return obj, nil
   218  }
   219  
   220  // GetPruneObjs calculates the set of prune objects, and retrieves them
   221  // from the cluster. Set of prune objects equals the set of inventory
   222  // objects minus the set of currently applied objects. Returns an error
   223  // if one occurs.
   224  func (p *Pruner) GetPruneObjs(
   225  	inv inventory.Info,
   226  	objs object.UnstructuredSet,
   227  	opts Options,
   228  ) (object.UnstructuredSet, error) {
   229  	ids := object.UnstructuredSetToObjMetadataSet(objs)
   230  	invIDs, err := p.InvClient.GetClusterObjs(inv)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	// only return objects that were in the inventory but not in the object set
   235  	ids = invIDs.Diff(ids)
   236  	objs = object.UnstructuredSet{}
   237  	for _, id := range ids {
   238  		pruneObj, err := p.getObject(id)
   239  		if err != nil {
   240  			if meta.IsNoMatchError(err) {
   241  				klog.V(4).Infof("skip pruning (object: %q): resource type not registered", id)
   242  				continue
   243  			}
   244  			if apierrors.IsNotFound(err) {
   245  				klog.V(4).Infof("skip pruning (object: %q): resource not found", id)
   246  				continue
   247  			}
   248  			return nil, err
   249  		}
   250  		objs = append(objs, pruneObj)
   251  	}
   252  	return objs, nil
   253  }
   254  
   255  func (p *Pruner) getObject(id object.ObjMetadata) (*unstructured.Unstructured, error) {
   256  	namespacedClient, err := p.namespacedClient(id)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	return namespacedClient.Get(context.TODO(), id.Name, metav1.GetOptions{})
   261  }
   262  
   263  func (p *Pruner) deleteObject(id object.ObjMetadata, opts metav1.DeleteOptions) error {
   264  	namespacedClient, err := p.namespacedClient(id)
   265  	if err != nil {
   266  		return err
   267  	}
   268  	return namespacedClient.Delete(context.TODO(), id.Name, opts)
   269  }
   270  
   271  func (p *Pruner) namespacedClient(id object.ObjMetadata) (dynamic.ResourceInterface, error) {
   272  	mapping, err := p.Mapper.RESTMapping(id.GroupKind)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	return p.Client.Resource(mapping.Resource).Namespace(id.Namespace), nil
   277  }
   278  

View as plain text