Skip to content

Controller

Controllers (pkg/controller) use events (pkg/event) to eventually trigger reconcile requests. They may be constructed manually, but are often constructed with a Builder (pkg/builder), which eases the wiring of event sources (pkg/source), like Kubernetes API object changes, to event handlers (pkg/handler), like "enqueue a reconcile request for the object owner". Predicates (pkg/predicate) can be used to filter which events actually trigger reconciles. There are pre-written utilities for the common cases, and interfaces and helpers for advanced cases.

Controller interface

Controller embeds reconcile.Reconciler.

type Controller interface {
    // Reconciler is called to reconcile an object by Namespace/Name
    reconcile.Reconciler

    // Watch takes events provided by a Source and uses the EventHandler to
    // enqueue reconcile.Requests in response to the events.
    //
    // Watch may be provided one or more Predicates to filter events before
    // they are given to the EventHandler.  Events will be passed to the
    // EventHandler if all provided Predicates evaluate to true.
    Watch(src source.Source, eventhandler handler.EventHandler, predicates ...predicate.Predicate) error

    // Start starts the controller.  Start blocks until the context is closed or a
    // controller has an error starting.
    Start(ctx context.Context) error

    // GetLogger returns this controller logger prefilled with basic information.
    GetLogger() logr.Logger
}

Controller type

type Controller struct {
    Name string
    MaxConcurrentReconciles int
    Do reconcile.Reconciler
    MakeQueue func() workqueue.RateLimitingInterface
    Queue workqueue.RateLimitingInterface
    SetFields func(i interface{}) error
    mu sync.Mutex
    Started bool
    ctx context.Context
    CacheSyncTimeout time.Duration
    startWatches []watchDescription
    LogConstructor func(request *reconcile.Request) logr.Logger
    RecoverPanic bool
}
  1. Do: reconcile.Reconciler

How Controller is used

Interestingly, New requires Manager and when a controller is created, the controller is added to the manager. A Controller must be run by a Manager. A controller is added to controllerManager via builder, more specifically NewControllerManagedBy. For more details, you can also check manager.

// New returns a new Controller registered with the Manager.  The Manager will ensure that shared Caches have
// been synced before the Controller is Started.
func New(name string, mgr manager.Manager, options Options) (Controller, error) {
    c, err := NewUnmanaged(name, mgr, options)
    if err != nil {
        return nil, err
    }

    // Add the controller as a Manager components
    return c, mgr.Add(c)
}
  1. Manager and Reconciler need to be prepared.
  2. Call NewControllerManagedBy(mgr) and Complete(r) with the manager and reconciler.
  3. Builer.build calls bldr.doController to create a controller with Controller.New.
    1. Inject dependencies to reconciler with Manager.SetFields(reconciler) (ref)
    2. Inititialize Controller
      &controller.Controller{
          Do: options.Reconciler,
          MakeQueue: func() workqueue.RateLimitingInterface {
              return workqueue.NewNamedRateLimitingQueue(options.RateLimiter, name)
          },
          MaxConcurrentReconciles: options.MaxConcurrentReconciles,
          CacheSyncTimeout:        options.CacheSyncTimeout,
          SetFields:               mgr.SetFields,
          Name:                    name,
          LogConstructor:          options.LogConstructor,
          RecoverPanic:            options.RecoverPanic,
      }
      
      1. Manager.SetFields is passed to ctrl.SetFields
      2. workqueue.NewNamedRateLimitingQueue is set to ctrl.MakeQueue
      3. Reconciler is set to ctrl.Do
    3. Register the controller to manager with mgr.Add(controller) (ref)
    4. The created controller is set to bldr.ctrl (ref)
  4. Builder.build also calls bldr.doWatch
    1. Kind is created for For and Owns.
    2. EventHandler is created.
    3. Call bldr.ctrl.Watch(src, hdlr, allPredicates...)
  5. controller.Watch
    1. Inject the cache to the src with SetFields (given by the Manager).
    2. Inject (sth) to the eventHandler and Predicates with SetFields (not checked what's injected).
    3. If the controller hasn't started yet, store the watches locally (startWatches) and return.
    4. If the controller has started, calls src.Start
  6. controller.Start is called by the Manager when Manager.Start is called.
    1. Controller can be started only once.
    2. Create a Queue with MakeQueue func which is specified in New.
    3. For the stored watches (startWatches), call watch.src.Start to start src to monitor API server and enqueue modified objects.
    4. Convert Kind to syncingSource and call syncingSource.WaitForSync. This waits until the cache is synced in src.Start by checking ks.started channel.
    5. Clean up the startWatches after the caches of all the watches are in-sync.
    6. Call processNextWorkItem with MaxConcurrentReconciles go routines.

Watch func

  1. Where is Watch called?
    1. Watch is called in bldr.doWatch in builder for For, Owns, and Watches configured with a controller builder.
  2. Watch calls SetFields for Source, EventHandler, and Predicates.
    if err := c.SetFields(src); err != nil {
        return err
    }
    if err := c.SetFields(evthdler); err != nil {
        return err
    }
    for _, pr := range prct {
        if err := c.SetFields(pr); err != nil {
            return err
        }
    }
    
    1. SetFields is one of the Controller's field SetFields func(i interface{}) error, which is set when initializing in NewUnmanaged from mgr.SetFields.
    2. For more details, you can check inject and manager

Start func

  1. Calls processNextWorkItem until it returns false here.
  2. Where is Start called? -> Called from manager.