Deleter
Description
Deleter allows resources to customize how the reconciler deletes them from Azure. This extension is invoked when a resource has a deletion timestamp in Kubernetes (indicating the user wants to delete it) and gives the resource control over the deletion process.
The interface is called after Kubernetes marks the resource for deletion but before the standard ARM DELETE operation. This allows resources to perform cleanup, handle special deletion scenarios, or coordinate multiple deletion operations.
Interface Definition
See the Deleter interface definition in the source code.
Motivation
The Deleter extension exists to handle cases where:
- Pre-deletion operations: Resources that need to perform cleanup before being deleted from Azure (e.g., canceling subscriptions, disabling features)
- Multi-step deletion: Resources requiring multiple API calls in a specific order to delete properly
- Dependent resource cleanup: Resources that need to ensure dependent resources are handled before deletion
- Soft-delete handling: Resources with soft-delete capabilities that may need special deletion modes
- Conditional deletion: Resources that should skip Azure deletion under certain circumstances (e.g., externally managed resources)
- Coordinated deletion: Resources that need to coordinate with other Azure services during deletion
When to Use
Implement Deleter when:
- ✅ Pre-deletion operations must be performed (e.g., canceling, disabling)
- ✅ Multiple Azure API calls are needed for complete deletion
- ✅ Deletion order matters across related resources
- ✅ Custom error handling is needed during deletion
- ✅ Soft-delete or purge operations require special logic
- ✅ The resource should be preserved in Azure in some scenarios
Do not use Deleter when:
- ❌ The standard DELETE operation works correctly
- ❌ You only need to clean up Kubernetes resources (use finalizers)
- ❌ The logic should apply to all resources (modify the controller)
- ❌ You’re working around an Azure API bug (fix/report the bug)
Example: Subscription Alias Deletion
See the full implementation in alias_extensions.go.
Key aspects of this implementation:
- Type assertions: For both resource type and hub version
- Conditional logic: Checks if subscription ID is available
- Pre-deletion operation: Cancels subscription before deleting alias
- Error handling: Returns errors that prevent finalizer removal
- Chain pattern: Calls
next()to perform standard deletion - Logging: Clear logging of each step for debugging
Common Patterns
Pattern 1: Simple Pre-deletion Operation
func (ex *ResourceExtension) Delete(
ctx context.Context,
log logr.Logger,
resolver *resolver.Resolver,
armClient *genericarmclient.GenericClient,
obj genruntime.ARMMetaObject,
next extensions.DeleteFunc,
) (ctrl.Result, error) {
resource := obj.(*myservice.MyResource)
// Perform cleanup operation
log.V(Status).Info("Performing pre-deletion cleanup")
if err := ex.performCleanup(ctx, resource, armClient); err != nil {
return ctrl.Result{}, eris.Wrap(err, "cleanup failed")
}
// Proceed with standard deletion
return next(ctx, log, resolver, armClient, obj)
}
Pattern 2: Conditional Deletion
func (ex *ResourceExtension) Delete(
ctx context.Context,
log logr.Logger,
resolver *resolver.Resolver,
armClient *genericarmclient.GenericClient,
obj genruntime.ARMMetaObject,
next extensions.DeleteFunc,
) (ctrl.Result, error) {
resource := obj.(*myservice.MyResource)
// Check if resource should be preserved in Azure
if ex.shouldPreserve(resource) {
log.V(Status).Info("Skipping Azure deletion, resource marked for preservation")
// Return success without calling next() - finalizer will be removed
return ctrl.Result{}, nil
}
// Proceed with normal deletion
return next(ctx, log, resolver, armClient, obj)
}
Pattern 3: Soft Delete with Purge Option
func (ex *ResourceExtension) Delete(
ctx context.Context,
log logr.Logger,
resolver *resolver.Resolver,
armClient *genericarmclient.GenericClient,
obj genruntime.ARMMetaObject,
next extensions.DeleteFunc,
) (ctrl.Result, error) {
resource := obj.(*myservice.MyResource)
// Perform standard deletion (moves to soft-deleted state)
result, err := next(ctx, log, resolver, armClient, obj)
if err != nil {
return result, err
}
// If purge is requested, purge the soft-deleted resource
if resource.Spec.DeleteMode != nil && *resource.Spec.DeleteMode == "Purge" {
log.V(Status).Info("Purging soft-deleted resource")
if err := ex.purgeResource(ctx, resource, armClient); err != nil {
return ctrl.Result{}, eris.Wrap(err, "failed to purge resource")
}
}
return ctrl.Result{}, nil
}
Deletion Lifecycle
Understanding the deletion process:
- User deletes resource:
kubectl deletesets deletion timestamp - Finalizer blocks deletion: ASO finalizer prevents immediate removal from Kubernetes
- Deleter invoked: Custom
Delete()method is called - Pre-deletion logic: Extension performs custom operations
- Standard deletion:
next()sends DELETE to ARM - ARM deletion completes: Azure resource is removed
- Finalizer removed: Kubernetes removes the resource
If any step fails, the process pauses and will retry on the next reconciliation.
Error Handling
Proper error handling in deleters is critical:
// Transient error - will retry
return ctrl.Result{}, eris.Wrap(err, "temporary failure")
// Permanent error with condition
return ctrl.Result{}, conditions.NewReadyConditionImpactingError(
err,
conditions.ConditionSeverityError,
conditions.ReasonFailed)
// Requeue for later retry
return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
// Success
return ctrl.Result{}, nil
Testing
When testing Deleter extensions:
- Test successful deletion: Verify the happy path works
- Test pre-deletion operations: Ensure cleanup logic executes
- Test error scenarios: Verify error handling prevents finalizer removal
- Test idempotency: Multiple calls should be safe
- Test conditional paths: Cover all branching logic
- Test requeue behavior: Verify multi-step deletions requeue correctly
Important Notes
- Always call
next()unless: You have a very specific reason to skip Azure deletion - Handle missing IDs gracefully: Resource might not have been created in Azure yet
- Return appropriate Results: Use
RequeueAfterfor async operations - Log clearly: Deletion issues are hard to debug, good logging helps
- Be idempotent: Deletion might be called multiple times
- Don’t leak resources: Ensure Azure resources are eventually deleted
Related Extension Points
- PreReconciliationChecker: Validate before operations
- PostReconciliationChecker: Validate after operations
- SuccessfulCreationHandler: Handle successful creation