<h1>Overview</h1>
<p>I am writing a Kubernetes controller for a VerticalScaler CRD that can vertically scale a Deployment in the cluster. My spec references an existing Deployment object in the cluster. I'd like to enqueue a reconcile request for a VerticalScaler if the referenced Deployment is modified or deleted.</p>
<pre class="lang-golang prettyprint-override"><code>// VerticalScalerSpec defines the desired state of VerticalScaler.
type VerticalScalerSpec struct {
    // Name of the Deployment object which will be auto-scaled.
    DeploymentName string `json:&quot;deploymentName&quot;`
}
</code></pre>
<h2>Question</h2>
<p>Is there a good way to watch an arbitrary resource when that resource is not owned by the controller, and the resource does not hold a reference to the object whose resource is managed by the controller?</p>
<h1>What I Found</h1>
<p>I think this should be configured in the Kubebuilder-standard <a href="https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/#resources-watched-by-the-controller" rel="noreferrer">SetupWithManager</a> function for the controller, though it's possible a watch could be set up someplace else.</p>
<pre class="lang-golang prettyprint-override"><code>// SetupWithManager sets up the controller with the Manager.
func (r *VerticalScalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&amp;v1beta1.VerticalScaler{}).
        Complete(r)
}
</code></pre>
<p>I've been searching for a good approach in <a href="https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/builder" rel="noreferrer">controller-runtime/pkg/builder</a> and the Kubebuilder docs. The closest example I found was the section &quot;Watching Arbitrary Resources&quot; in the <a href="https://book-v1.book.kubebuilder.io/beyond_basics/controller_watches.html" rel="noreferrer">kubebuilder-v1 docs on watches</a>:</p>
<blockquote>
<p>Controllers may watch arbitrary Resources and map them to a key of the Resource managed by the controller. Controllers may even map an event to multiple keys, triggering Reconciles for each key.</p>
<p>Example: To respond to cluster scaling events (e.g. the deletion or addition of Nodes), a Controller would watch Nodes and map the watch events to keys of objects managed by the controller.</p>
</blockquote>
<p>My challenge is how to map the Deployment to the depending VerticalScaler(s), since this information is not present on the Deployment. I could <a href="https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client#hdr-Indexing" rel="noreferrer">create an index</a> on the VerticalScaler and look up depending VerticalScalers from the <a href="https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/handler#MapFunc" rel="noreferrer">MapFunc</a> using a field selector, but it doesn't seem like I should do I/O inside a MapFunc. If the list-Deployments operation failed I would be unable to retry or re-enqueue the change.</p>
<p>I have this code working using this imperfect approach:</p>
<pre class="lang-golang prettyprint-override"><code>const deploymentNameIndexField = &quot;.metadata.deploymentName&quot;

// SetupWithManager sets up the controller with the Manager.
func (r *VerticalScalerReconciler) SetupWithManager(mgr ctrl.Manager) error {
    if err := r.createIndices(mgr); err != nil {
        return err
    }

    return ctrl.NewControllerManagedBy(mgr).
        For(&amp;v1beta1.VerticalScaler{}).
        Watches(
            &amp;source.Kind{Type: &amp;appsv1.Deployment{}},
            handler.EnqueueRequestsFromMapFunc(r.mapDeploymentToRequests)).
        Complete(r)
}

func (r *VerticalScalerReconciler) createIndices(mgr ctrl.Manager) error {
    return mgr.GetFieldIndexer().IndexField(
        context.Background(),
        &amp;v1beta1.VerticalScaler{},
        deploymentNameIndexField,
        func(object client.Object) []string {
            vs := object.(*v1beta1.VerticalScaler)

            if vs.Spec.DeploymentName == &quot;&quot; {
                return nil
            }

            return []string{vs.Spec.DeploymentName}
        })
}

func (r *VerticalScalerReconciler) mapDeploymentToRequests(object client.Object) []reconcile.Request {
    deployment := object.(*appsv1.Deployment)

    ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
    defer cancel()

    var vsList v1beta1.VerticalScalerList

    if err := r.List(ctx, &amp;vsList,
        client.InNamespace(deployment.Namespace),
        client.MatchingFields{deploymentNameIndexField: deployment.Name},
    ); err != nil {
        r.Log.Error(err, &quot;could not list VerticalScalers. &quot; +
            &quot;change to Deployment %s.%s will not be reconciled.&quot;,
            deployment.Name, deployment.Namespace)
        return nil
    }

    requests := make([]reconcile.Request, len(vsList.Items))

    for i, vs := range vsList.Items {
        requests[i] = reconcile.Request{
            NamespacedName: client.ObjectKeyFromObject(&amp;vs),
        }
    }

    return requests
}
</code></pre>
<h2>Other Considered Approaches</h2>
<p>Just to cover my bases I should mention I don't want to set the VerticalScaler as an owner of the Deployment because I don't want to garbage collect the Deployment if the VerticalScaler is deleted. Even a non-controller ownerReference causes garbage collection.</p>
<p>I also considered using a <a href="https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/source#Channel" rel="noreferrer">Channel</a> watcher, but the docs say that is for events originating from outside the cluster, which this is not.</p>
<p>I could also create a separate controller for the Deployment, and update some field on the depending VerticalScaler(s) from that controller's Reconcile function, but then I would also need a finalizer to handle triggering a VerticalScaler reconcile when a Deployment is deleted, and that seems like overkill.</p>
<p>I could have my VerticalScaler reconciler add an annotation to the Deployment, but there's a probability that the Deployment annotations can be overwritten if managed by for example Helm. That also would not cause a reconcile request in the case where the VerticalScaler is created before the Deployment.</p>

Adam Snyder
<p>As an alternative to <code>EnqueueRequestsFromMapFunc</code>, you can use:</p>
<pre class="lang-golang prettyprint-override"><code>ctrl.NewControllerManagedBy(mgr).
    For(&amp;v1beta1.VerticalScaler{}).
    Watches(
        &amp;source.Kind{Type: &amp;appsv1.Deployment{}},
        handler.Funcs{CreateFunc: r.CreateFunc})...
</code></pre>
<p>The handler's callback functions such as the <code>CreateFunc</code> above that you'd define, has the signature <code>func(event.CreateEvent, workqueue.RateLimitingInterface)</code>, giving you direct access to the workqueue. By default if you don't call <code>Done()</code> on the workqueue it will get requeued with exponential backoff. This should allow you to handle errors with io operations.</p>
