# Adopt controller-runtime

This document proposes to use controller-runtime whenever possible instead of client-go, since controller runtime provides several high level abstractions that helps avoid common client mistakes.

## Read operations are always cached

`client-go`:

```go
shootFactory := gardencoreinformers.NewSharedInformerFactoryWithOptions(gardenCli, time.Minute*10, gardencoreinformers.WithNamespace(namespace))
shootInformer := shootFactory.Core().V1beta1().Shoots()
shootLister := shootInformer.Lister()
shoot, err := controller.shootLister.Shoots("garden-nightly").Get("8be6476c")
```

`client.Client`

```go
// read from cache by default
shoot = &v1beta1.Shoot{}
err = cli.Get(context.TODO(), types.NamespacedName{Namespace: "garden-nightly", Name: "8be6476c"}, shoot)
```

Advantages:

- `client.Client` abstracts the informer cache away and read from cache whenever possible.
- `client.Client` ensures that there will be at most one informer for each GVK(group version kind).

## Reconciler interface encapsulates controller best practices

`client-go`:

```go

func (c *controller) syncHandler(key string) error {
	// real reconcile logic
}

// boilerplate below
func startController(log *zap.Logger, namespace string, gardenCli versioned.Interface, deleteFun deleteFunc, updateFun cacheFunc, stop chan struct{}) {
	shootFactory := gardencoreinformers.NewSharedInformerFactoryWithOptions(gardenCli, time.Minute*10, gardencoreinformers.WithNamespace(namespace))
	shootInformer := shootFactory.Core().V1beta1().Shoots()

	controller := &controller{
		log:            log,
		namespace:      namespace,
		deleteFunc:     deleteFun,
		updateFunc:     updateFun,
		shootLister:    shootInformer.Lister(),
		clustersSynced: shootInformer.Informer().HasSynced,
		workqueue:      workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Shoots"),
	}
	go shootFactory.Start(stop)

	for v, synced := range shootFactory.WaitForCacheSync(wait.NeverStop) {
		if !synced {
			log.Fatal("error syncing informer", zap.Any("value", v))
		}
	}

	shootInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: controller.enqueueShoot,
		UpdateFunc: func(oldObj, newObj interface{}) {
			oldShoot := oldObj.(*gardencorev1beta1.Shoot)
			newShoot := newObj.(*gardencorev1beta1.Shoot)
			if oldShoot.ResourceVersion == newShoot.ResourceVersion {
				return
			}
			controller.enqueueShoot(newObj)
		},
		DeleteFunc: controller.enqueueShootForDelete,
	})

	go func() {
		panicIf.Error(controller.Run(5, stop))
	}()
}

// Run will set up the event handlers
func (c *controller) Run(threads int, stop <-chan struct{}) error {
	defer runtime.HandleCrash()
	defer c.workqueue.ShutDown()

	c.log.Info("start shoot control loop")

	if ok := cache.WaitForCacheSync(stop, c.clustersSynced); !ok {
		return fmt.Errorf("failed to wait to caches to sync")
	}

	for i := 0; i < threads; i++ {
		go wait.Until(c.runWorker, time.Second, stop)
	}

	log.Info("started workers")
	<-stop
	log.Info("shutting down workers")

	return nil
}

func (c *controller) enqueueShoot(obj interface{}) {
	m, err := meta.Accessor(obj)
	if err != nil {
		c.log.Warn("object has no meta", zap.Error(err))
		return
	}

	if m.GetNamespace() != c.namespace {
		return
	}

	var key string
	if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
		runtime.HandleError(err)
		return
	}

	c.workqueue.AddRateLimited(key)
}

func (c *controller) runWorker() {
	for c.processNextWorkItem() {
	}
}

func (c *controller) processNextWorkItem() bool {
	obj, shutdown := c.workqueue.Get()

	if shutdown {
		return false
	}

	err := func(obj interface{}) error {
		defer c.workqueue.Done(obj)

		var key string
		var ok bool

		if key, ok = obj.(string); !ok {
			c.workqueue.Forget(obj)
			runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
			return nil
		}

		if err := c.syncHandler(key); err != nil {
			return fmt.Errorf("error syncing '%s': %s", key, err.Error())
		}

		c.workqueue.Forget(obj)
		return nil
	}(obj)
	if err != nil {
		runtime.HandleError(err)
		return true
	}

	return true
}
```

`Reconciler`

```go
func (r *ShootReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	// real reconcile logic

	return ctrl.Result{}, nil
}

// boilerplate below
func (r *ShootReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&v1beta1.Shoot{}).
		Complete(r)
}

if err = (&controllers.ShootReconciler{
    Client: mgr.GetClient(),
    Log:    ctrl.Log.WithName("controllers").WithName("Shoot"),
    Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "Shoot")
    os.Exit(1)
}
```

Advantages:
- Boilerplate is greatly reduced
- Leader elections are abstracted by `ctrl.Manager`
- `workerQueue` is maintained by `ctrl.Manager` carefully
- Requeue operation can be fine-grained controlled `ctrl.Result{}`, e.g. `ctrl.Result{RequeueAfter: 5*time.Minute}`
- The vanilla controller will be more complex if we have to watch multiple resources, but with `Reconciler` we simply declare the `Reconciler` ownes more resources:

```
func (r *ShootReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&v1beta1.Shoot{}).
		Owns(&corev1.Node).
		Owns(&corev1.Pod).
		Complete(r)
}
```
