Background
As I recently posted, I’ve worked on a couple of projects that have involved using Kubebuilder to create Kubernetes operators.
In last post we looked at using event filters to prevent delete notifications being processed by the reconciliation loop. Another thing I’ve noticed as I’ve been developing operators is that the flow when an object is created is typically: receive create notification, take some action, update the object Status
property. It turns out that updating the Status
triggers the reconciliation loop again (this also happens when you add a finalizer)! In this post we’ll look at filtering out these updates that occur due to the Status
object changing.
Implementing the filter
Here’s the code we had last time with an UpdateFunc
added:
func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&batch.CronJob{}).
WithEventFilter(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
// TODO - add filter logic here!
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
// The reconciler adds a finalizer so we perform clean-up
// when the delete timestamp is added
// Suppress Delete events to avoid filtering them out in the Reconcile function
return false
},
}).
Complete(r)
}
In the delete case, we simply returned false
to suppress all delete notifications. For the update case, we want to process any changes to the Spec
, but ignore Status
changes.
Fortunately, there is a metadata field that we can make use of. From the Custom Resource Definition docs:
The .metadata.generation value is incremented for all changes, except for changes to .metadata or .status
The Generation
metadata property is perfect: it will be updated when the Spec
changes and we have access to the metadata for both old and new objects in the UpdateFunc
:
func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&batch.CronJob{}).
WithEventFilter(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldGeneration := e.MetaOld.GetGeneration()
newGeneration := e.MetaNew.GetGeneration()
// Generation is only updated on spec changes (also on deletion),
// not metadata or status
// Filter out events where the generation hasn't changed to
// avoid being triggered by status updates
return oldGeneration != newGeneration
},
DeleteFunc: func(e event.DeleteEvent) bool {
// The reconciler adds a finalizer so we perform clean-up
// when the delete timestamp is added
// Suppress Delete events to avoid filtering them out in the Reconcile function
return false
},
}).
Complete(r)
}
Conclusion
With this extra event filter, we now have better control over when the reconciliation loop is triggered. It will no longer be triggered by adding a finalizer or updating the Status
, but will still be triggereed by external changes to the Spec
or by our code scheduling a Requeue.