Controller Example

This chapter walks through a simple Controller implementation.

This example is for the Controller for the ContainerSet API shown in the Resource Example.It uses the controller-runtime librariesto implement the Controller and Manager.

Unlike the Hello World example, here we use the underlying Controller libraries directly insteadof the higher-level application pattern libraries. This gives greater control overthe Controller is configured.

$ kubebuilder create api —group workloads —version v1beta1 —kind ContainerSet

pkg/controller/containerset/containerset_controller.go

Setup

ContainerSetController

ContainerSetController has a single annotation:

  • // +kubebuilder:rbac creates RBAC rules in the config/rbac/rbac_role.yaml file when make is run.This will ensure the Kubernetes ServiceAccount running the controller can read / write to the Deployment API.ContainerSetController has 2 variables:

  • client.Client is a client for reading / writing Kubernetes APIs.

  • scheme *runtime.Scheme is a runtime.Scheme used by the library to set OwnerReferences.

Adding a Controller to the Manager

Add creates a new Controller that will be started by the Manager. When adding a Controller it is important to setupWatch functions to trigger Reconciles.

Watch is a function that takes an event source.Source and a handler.EventHandler. The Source provides eventsfor some type, and the EventHandler responds to events by enqueuing reconcile.Requests for objects.Watch optionally takes a list of Predicates that may be used to filter events.

Sources

  • To watch for create / update / delete events for an object use a source.KindSource e.g.source.KindSource{Type: &v1.Pod}Handlers

  • To enqueue a Reconcile for the object in the event use a handler.EnqueueRequestForObject

  • To enqueue a Reconcile for the owner object that created the object in the event use a handler.EnqueueRequestForOwnerwith the type of the owner e.g. &handler.EnqueueRequestForOwner{OwnerType: &appsv1.Deployment{}, IsController: true}
  • To enqueue Reconcile requests for an arbitrary collection of objects in response to the event, use ahandler.EnqueueRequestsFromMapFunc.Example:

  • Create a new ContainerSetController struct that will.

    • Invoke Reconcile with the Name and Namespace of a ContainerSet for ContainerSet create / update / delete events
    • Invoke Reconcile with the Name and Namespace of a ContainerSet for Deployment create / update / delete events

Reference

  1. type ContainerSetController struct {
  2. client.Client
  3. scheme *runtime.Scheme
  4. }
  5. func Add(mgr manager.Manager) error (
  6. // Create a new Controller
  7. c, err := controller.New("containerset-controller", mgr,
  8. controller.Options{Reconciler: &ContainerSetController{
  9. Client: mgr.GetClient(),
  10. scheme: mgr.GetScheme(),
  11. }})
  12. if err != nil {
  13. return err
  14. }
  15. // Watch for changes to ContainerSet
  16. err = c.Watch(
  17. &source.Kind{Type:&workloadsv1beta1.ContainerSet{}},
  18. &handler.EnqueueRequestForObject{})
  19. if err != nil {
  20. return err
  21. }
  22. // Watch for changes to Deployments created by a ContainerSet and trigger a Reconcile for the owner
  23. err = c.Watch(
  24. &source.Kind{Type: &appsv1.Deployment{}},
  25. &handler.EnqueueRequestForOwner{
  26. IsController: true,
  27. OwnerType: &workloadsv1beta1.ContainerSet{},
  28. })
  29. if err != nil {
  30. return err
  31. }
  32. return nil
  33. }

Adding Annotations For Watches And CRUD Operations

It is important// +kubebuilder:rbac annotations when adding Watches or CRUD operationsso that when the Controller is deployed it will have the correct permissions.

make must be run anytime annotations are changed to regenerated code and configs.

Implementing Controller Reconcile

Level vs Edge

The Reconcile function does not differentiate between create, update or deletion events.Instead it simply reads the state of the cluster at the time it is called.

Reconcile uses a client.Client to read and write objects. The Client is able toread or write any type of runtime.Object (e.g. Kubernetes object), so users don't needto generate separate clients for each collection of APIs.

The business logic of the Controller is implemented in the Reconcile function. This function takes the Namespace and Name of a ContainerSet, allowing multiple Events to be batched together into a single Reconcile call.

The function shown here creates or updates a Deployment using the replicas and image specified inContainerSet.Spec. Note that it sets an OwnerReference for the Deployment to enable garbage collectionon the Deployment once the ContainerSet is deleted.

  • Read the ContainerSet using the NamespacedName
  • If there is an error or it has been deleted, return
  • Create the new desired DeploymentSpec from the ContainerSetSpec
  • Read the Deployment and compare the Deployment.Spec to the ContainerSet.Spec
  • If the observed Deployment.Spec does not match the desired spec
    • Deployment was not found: create a new Deployment
    • Deployment was found and changes are needed: update the Deployment
  1. var _ reconcile.Reconciler = &ContainerSetController{}
  2. func (r *ReconcileContainerSet) Reconcile(request reconcile.Request) (reconcile.Result, error) {
  3. instance := &workloadsv1beta1.ContainerSet{}
  4. err := r.Get(context.TODO(), request.NamespacedName, instance)
  5. if err != nil {
  6. if errors.IsNotFound(err) {
  7. // Object not found, return. Created objects are automatically garbage collected.
  8. // For additional cleanup logic use finalizers.
  9. return reconcile.Result{}, nil
  10. }
  11. // Error reading the object - requeue the request.
  12. return reconcile.Result{}, err
  13. }
  14. // TODO(user): Change this to be the object type created by your controller
  15. // Define the desired Deployment object
  16. deploy := &appsv1.Deployment{
  17. ObjectMeta: metav1.ObjectMeta{
  18. Name: instance.Name + "-deployment",
  19. Namespace: instance.Namespace,
  20. },
  21. Spec: appsv1.DeploymentSpec{
  22. Selector: &metav1.LabelSelector{
  23. MatchLabels: map[string]string{"deployment": instance.Name + "-deployment"},
  24. },
  25. Replicas: &instance.Spec.Replicas,
  26. Template: corev1.PodTemplateSpec{
  27. ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"deployment": instance.Name + "-deployment"}},
  28. Spec: corev1.PodSpec{
  29. Containers: []corev1.Container{
  30. {
  31. Name: instance.Name,
  32. Image: instance.Spec.Image,
  33. },
  34. },
  35. },
  36. },
  37. },
  38. }
  39. if err := controllerutil.SetControllerReference(instance, deploy, r.scheme); err != nil {
  40. return reconcile.Result{}, err
  41. }
  42. // TODO(user): Change this for the object type created by your controller
  43. // Check if the Deployment already exists
  44. found := &appsv1.Deployment{}
  45. err = r.Get(context.TODO(), types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, found)
  46. if err != nil && errors.IsNotFound(err) {
  47. log.Printf("Creating Deployment %s/%s\n", deploy.Namespace, deploy.Name)
  48. err = r.Create(context.TODO(), deploy)
  49. if err != nil {
  50. return reconcile.Result{}, err
  51. }
  52. } else if err != nil {
  53. return reconcile.Result{}, err
  54. }
  55. // TODO(user): Change this for the object type created by your controller
  56. // Update the found object and write the result back if there are any changes
  57. if !reflect.DeepEqual(deploy.Spec, found.Spec) {
  58. found.Spec = deploy.Spec
  59. log.Printf("Updating Deployment %s/%s\n", deploy.Namespace, deploy.Name)
  60. err = r.Update(context.TODO(), found)
  61. if err != nil {
  62. return reconcile.Result{}, err
  63. }
  64. }
  65. return reconcile.Result{}, nil
  66. }