Enqueue! Your father was a MapFunc
Typically when creating a controller, you’ll make use of the fairly standard controller builder pattern –
func (r FunsiesReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1.Funsies{}).
Complete(r)
func (r *FunsiesReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
return ctrl.Result{}, nil
}
This allows us to set up watches on v1.Funsies
kinds so we can handle them appropriately in our Reconcile method.
But what happens if we want to trigger a Reconcile for all Funsiess based on some other object like corev1.Namespaces
?
Watches and EnqueueRequestsFromMapFunc
The builder provides a builder.Watches
method that allows us to specify another object that we’d like to watch.
Watches
takes the kind we are watching, an event handler to handle the logic of what we want to do when we get an event for the object, and additionally options to further modify how the watcher behaves.
Watches(object client.Object, eventhandler handler.EventHandler, opts ...WatchesOption)`
Since we are wanting to react to namespace events, we know our first parameter will be &corev1.Namespace{}
.
Next, we need to tell the watcher what to do once it gets an event for the namespace.
handler.EnqueueRequestsFromMapFunc
is our helper here.
EnqueueRequestsFromMapFunc
accepts a handler.MapFunc
which allows us to control how we want to handle that watched object.
The arguments for the handler.MapFunc
are only a context and the object we are watching.
This works for some simple one-to-one matching or translations from the watched object to the object we are reconciling.
For example, if we only had a single v1.Funsies
object per namespace that always ended with a constant suffix.
// We'll reuse the example in the docs for this instance
// https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/handler#example-EnqueueRequestsFromMapFunc
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request {
return []reconcile.Request{
{NamespacedName: types.NamespacedName{
Name: a.GetName() + "-1",
Namespace: a.GetNamespace(),
}},
{NamespacedName: types.NamespacedName{
Name: a.GetName() + "-2",
Namespace: a.GetNamespace(),
}},
}
})
But if we needed to do slightly more complicated interactions here, we can rely on closures to pass through additional useful parameters.
Let’s say our desired outcome is to reconcile all v1.Funsies
objects in response to a corev1.Namespace
event.
We can accomplish this with passing through the mgr.Client()
and returning a handler.MapFunc
.
We an call our Watches method like the following:
Watches(
&corev1.Namespace{},
handler.EnqueueRequestsFromMapFunc(lookupFunsiessInNamespace(mgr.GetClient())),
)
And then handle the necessary logic (lookups) in the lookupFunsiesInNamespace
function:
func lookupFunsiessInNamespace(c client.Client) func(ctx context.Context, a client.Object) []reconcile.Request {
return func(ctx context.Context, a client.Object) []reconcile.Request {
squigglyList := &v1.FunsiesList{}
if err := c.List(ctx, squigglyList, client.InNamespace(a.GetName())); err != nil {
log.FromContext(ctx).Error(err, "unable to list squigglys in namespace", "namespace", a.GetName())
return []reconcile.Request{}
}
requests := make([]reconcile.Request, 0, len(squigglyList.Items))
for _, squiggly:= range squigglyList.Items {
requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: a.GetName(), Name: squiggly.GetName()}})
}
return requests
}
}
Bonus Tip
Additionally, if we need to get slightly fancier, we can provide handler.WatchesOption
s.
This would allow us to add in things like predicates to further customize the watch we are creating.
Watches(
&corev1.Namespace{},
handler.EnqueueRequestsFromMapFunc(reconcilePodsInNamespace(mgr.GetClient())),
builder.WithPredicates(
predicate.Or(
predicate.GenerationChangedPredicate{},
predicate.AnnotationChangedPredicate{},
),
),
)