controller-runtime helpers simplify adding, checking, and removing finalizers: each controller should only add and remove its own finalizer string. Using these helpers avoids manual metadata slice manipulation.
Finalizer name and placement
Use a unique, domain-style finalizer string and declare it as a package-level constant so the controller uses a single canonical value everywhere.The finalizer pattern in Reconcile
A well-structured Reconcile should clearly separate two paths:- Normal (non-deleting) path — ensure the finalizer is present.
- Deletion path — perform cleanup and remove the finalizer so Kubernetes can finish deletion.
- Only add the finalizer when the object is not being deleted (DeletionTimestamp == zero).
- When adding the finalizer, persist the object with
r.Updateand then requeue so the next reconcile will operate on the object with the updatedresourceVersion. - When deletion is in progress (DeletionTimestamp non-nil), only perform cleanup if your finalizer is present; then remove it and
Updateso the API server can complete deletion.
Add finalizer during normal reconciliation
Check the DeletionTimestamp and add the finalizer if missing. Usecontrollerutil.ContainsFinalizer and controllerutil.AddFinalizer to avoid unnecessary updates.
Deletion branch — perform cleanup, then remove finalizer
When the Resource’s DeletionTimestamp is set the API server has accepted a delete request but is holding the object until all finalizers are removed. Act only if your finalizer is present; perform any required controller-specific cleanup, then remove your finalizer andUpdate the object so Kubernetes can finish deletion.
A full Reconcile example that demonstrates both branches:
Do not remove finalizers that you do not own. Always check
ContainsFinalizer before calling RemoveFinalizer to avoid interfering with other controllers’ cleanup. Also return errors from Update so controller-runtime will retry on transient failures.Verify deletion completed
Once the controller has removed its finalizer and the API server finishes deletion, a subsequent get should return NotFound:NotFound response indicates the delete request completed successfully and the finalizer flow worked: your controller performed cleanup and released its finalizer so Kubernetes could complete deletion.
Quick summary and checklist
References
- Kubernetes finalizers
- Owners and dependents (ownerReferences)
- Kubernetes garbage collection
- controller-runtime controllerutil helpers