Digital Developer Conference: Cloud Security 2021 -- Build the skills to secure your cloud and data Register free

Explanation of the Credential Rotator Operator controller code

This article offers a detailed look at how custom controller code works. We describe the logic of the operator solution that was mentioned in the previous article of this learning path and are calling it the Credential Rotator Operator.

This article is not intended to be an introduction to all aspects of writing a Kubernetes Operator. Instead, we focus on where and how to implement a specific set of logic for a given task. In this case, the logic for rotating credentials.

By reading this article, you gain deep technical knowledge about:

  • The code that enables controllers to run
  • How the Reconcile loop works and how you can use it to manage Kubernetes resources
  • How processing of a custom resource (CR) can be broken down into more consumable parts
  • Basic Get, Update, and Create functions that are used to save resources to your Kubernetes cluster
  • How to generate Kubernetes events
  • KubeBuilder markers and how to use them to set role-based access control (RBAC)

As a reminder, the controller is the core part of Kubernetes that ensures that an object’s actual state matches its desired state.

Examine the code

This article details the custom controller code for the Credential Rotator Operator, which can be found in the following GitHub repo: github.com/IBM/credential-rotator-operator.

Key functional areas covered

  1. Reconcile function
  2. Resource management
  3. Resource key
  4. Web application
  5. Return types
  6. Kubernetes events
  7. KubeBuilder markers

1. Reconcile function

A controller’s Reconcile method contains the logic responsible for monitoring and applying the requested state for specific resource instances. The Reconciler sends client requests to Kubernetes APIs and runs every time a CR is modified by a user or changes state (for example, if a status is updated). If the Reconcile method fails, it can be requeued to run again.

We scaffolded the controller in this operator with the Operator SDK, and added the following logic to handle the credential rotation:

  1. Create a service resource key for the back-end service in question. This example uses an IBM Cloudant database deployed in the IBM Cloud.
  2. Update the Kubernetes Secret with the new resource key.
  3. Restart the web application instances.
  4. Delete the previous resource key for Cloudant in the IBM Cloud.

In the following code snippet, you can view the example controller’s Reconcile function. Don’t worry if you can’t follow all of it yet. The code is described in more detail in subsequent sections.

func (r *CredentialRotatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := ctrllog.FromContext(ctx)
    logger.Info("== Reconciling CredentialRotator")

    // Fetch the CredentialRotator instance
    instance := &securityv1alpha1.CredentialRotator{}
    err := r.Get(ctx, req.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request - return and don't requeue:
            return r.doNotRequeue()
        }
        // Error reading the object - requeue the request
        return r.requeueOnErr(err)
    }

    // If no phase set, default to pending (the initial phase)
    if instance.Status.Phase == "" {
        instance.Status.Phase = securityv1alpha1.PhasePending
    }

    // The different operations are broken down into phases:
    // PENDING -> CREATING -> NOTIFYING -> DELETING -> DONE
    // A phase seperates operations as follows :
    // 1. Creating resource key and creating/updating secret to store credentials
    // 2. Notifying app PODs of change to credentials by restarting them
    // 3. Deleting previous resource key which is replaced with newly created key
    switch instance.Status.Phase {

    case securityv1alpha1.PhasePending:
        logger.Info("Phase: PENDING")
        r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhasePending)
        instance.Status.Phase = securityv1alpha1.PhaseCreating

    case securityv1alpha1.PhaseCreating:
        logger.Info("Phase: CREATING")
        r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhaseCreating)
        icClient, err := ibmcloudclient.NewClient(instance.Spec.UserAPIKey)
        if err != nil {
            logger.Error(err, "Resource key creation failure")
            // Error creating resource key. Wait until it is fixed.
            return r.requeueOnErr(err)
        }
        resourceKey, err := icClient.CreateResourceKeyForServiceInstance(instance.Spec.ServiceGUID)
        if err != nil {
            logger.Error(err, "Resource key creation failure")
            // Error creating resource key. Wait until it is fixed.
            return r.requeueOnErr(err)
        }
        logger.Info("Resource key created", "ID", resourceKey.GUID)

        // If secret exists, first get the resource key ID and set it
        // to 'instance.Status.PreviousResourceKeyID' so that the
        // previous key can be removed in 'DELETING' phase. Then
        // update the secret with new key created in 'CREATING' phase.
        // Otherwise create secret adding the resource key created.
        secret := newSecretObject(*resourceKey.ID, *resourceKey.Credentials.Apikey, instance.Spec.ServiceURL, instance.Spec.AppNameSpace)
        found := &corev1.Secret{}
        err = r.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, found)
        if err != nil && errors.IsNotFound(err) { //Create secret
            err = r.Create(ctx, secret)
            if err != nil {
                // Error creating secret. Wait until it is fixed.
                return r.requeueOnErr(err)
            }
            logger.Info("Secret created", "Name", "Namespace", secret.Name, secret.Namespace)
        } else if err != nil {
            // Error getting secret. Wait until it is fixed.
            return r.requeueOnErr(err)
        } else { //Update secret
            instance.Status.PreviousResourceKeyID = string(found.Data["resourceKeyID"])
            updateSecretObject(found, resourceKey)
            err = r.Update(ctx, found)
            if err != nil {
                // Error updating secret. Wait until it is fixed.
                return r.requeueOnErr(err)
            }
            logger.Info("Secret updated", "Name", "Namespace", secret.Name, secret.Namespace)
        }
        instance.Status.Phase = securityv1alpha1.PhaseNotifying

    case securityv1alpha1.PhaseNotifying:
        logger.Info("Phase: Notifying")
        r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhaseNotifying)

        found := &appsv1.Deployment{}
        err = r.Get(ctx, types.NamespacedName{Name: instance.Spec.AppName, Namespace: instance.Spec.AppNameSpace}, found)
        if err != nil && errors.IsNotFound(err) { //Create secret
            logger.Info("No Deployment found", "Deployment", instance.Spec.AppName,
                "Namespace", instance.Spec.AppNameSpace)
        } else if err != nil {
            return r.requeueOnErr(err)
        } else { //Restart
            patch := []byte(fmt.Sprintf(`{"spec":{"template":{"metadata":{"labels":{"credentials-rotator-redeloyed":"%v"}}}}}`, time.Now().Unix()))
            err = r.Patch(ctx, found, client.RawPatch(types.StrategicMergePatchType, patch))
            if err != nil {
                // Error updating deployment. Wait until it is fixed.
                return r.requeueOnErr(err)
            }
            logger.Info("Deployment updated and restarted", "Deployment", instance.Spec.AppName,
                "Namespace", instance.Spec.AppNameSpace)
        }
        instance.Status.Phase = securityv1alpha1.PhaseDeleting

    case securityv1alpha1.PhaseDeleting:
        logger.Info("Phase: Deleting")
        r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhaseDeleting)

        icClient, err := ibmcloudclient.NewClient(instance.Spec.UserAPIKey)
        if err != nil {
            logger.Error(err, "Resource key creation failure")
            // Error creating resource key. Wait until it is fixed.
            return r.requeueOnErr(err)
        }
        deleteResourceKeyID := instance.Status.PreviousResourceKeyID
        if deleteResourceKeyID != "" {
            err := icClient.DeleteResourceKey(deleteResourceKeyID)
            if err != nil {
                logger.Error(err, "Resource key deletion failure")
                // Error deleting resource key. Wait until it is fixed.
                return r.requeueOnErr(err)
            }
            logger.Info("Previous resource key deleted", "ID", deleteResourceKeyID)
        } else {
            logger.Info("No previous resource key to delete")
        }
        instance.Status.Phase = securityv1alpha1.PhaseDone

    case securityv1alpha1.PhaseDone:
        logger.Info("Phase: DONE")
        r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhaseDone)
        return r.doNotRequeue()

    default:
        logger.Info("NOP")
        return r.doNotRequeue()
    }

    // Update the CredentialRotator instance, setting the status to the respective phase
    err = r.Status().Update(ctx, instance)
    if err != nil {
        return r.requeueOnErr(err)
    }

    return r.requeue()
}

We implement the logic in our controller in a phased approach:

PENDING -> CREATING -> NOTIFYING -> DELETING -> DONE

The phases perform the following operations:

  • PENDING: Receive CR request
  • CREATING: Create service resource key and update Secret
  • NOTIFYING: Ask web app deployment to restart its pods
  • DELETING: Delete previous service resource key
  • DONE: Finish handling CR request

After a phase is complete, it sets the next phase (per the operation order we just outlined). This continues until the DONE phase is reached.

The following snippet shows that the NOTIFYING phase is being set and the CR instance is updated to set its status to the new phase:

instance.Status.Phase = securityv1alpha1.PhaseNotifying
[...]
// Update the CredentialRotator instance, setting the status to the respective phase
err = r.Status().Update(ctx, instance)
if err != nil {
    return r.requeueOnErr(err)
}

return r.requeue()

The request is then requeued because the CR instance processing is not yet complete. There are still phases to be done.

The first part of the Reconcile function is to get the CR instance as follows:

// Fetch the CredentialRotator instance
instance := &securityv1alpha1.CredentialRotator{}
err := r.Get(ctx, req.NamespacedName, instance)
if err != nil {
    if errors.IsNotFound(err) {
        // Request object not found, could have been deleted after reconcile request - return and don't requeue:
        return r.doNotRequeue()
    }
    // Error reading the object - requeue the request
    return r.requeueOnErr(err)
}

By using this instance, the current phase can be read from the instance.Status.Phase property and can be used to start or continue processing the request. The CredentialRotator CR specification (CredentialRotatorSpec) and status (CredentialRotatorStatus) are defined in /api/v1alpha1/credentialrotator_types.go.

2. Resource management

This example uses resources on the IBM Cloud, so we use the IBM Cloud Resource Controller Go client to manage them. The code for this is handled in the pkg/ibmcloudclient package.

The following code abstracts the details of using the client, the IBM Cloud Resource Controller:

package ibmcloudclient

import (
    "fmt"
    "strings"
    "time"

    "github.com/pkg/errors"

    "github.com/IBM/go-sdk-core/v5/core"
    "github.com/IBM/platform-services-go-sdk/resourcecontrollerv2"
)

type ibmCloudClient struct {
    resourceControllerService *resourcecontrollerv2.ResourceControllerV2
}

// NewClient creates our client wrapper object for interacting with IBM Cloud
func NewClient(userAPIKey string) (*ibmCloudClient, error) {
    resourceControllerService, err := initResourceController(userAPIKey)
    if err != nil {
        return nil, errors.Wrap(err, "unable to connect to IBM Cloud resource controller service")
    }

    return &ibmCloudClient{
        resourceControllerService: resourceControllerService,
    }, nil
}

// CreateResourceKeyForServiceInstance Create resource key for a service on the IBM Cloud
func (c *ibmCloudClient) CreateResourceKeyForServiceInstance(serviceGUID string) (*resourcecontrollerv2.ResourceKey, error) {
    t := time.Now()
    var keyName = "creds_for_" + strings.Split(serviceGUID, "-")[0] + "_" + t.Format("20060102150405")

    createResourceKeyOptions := c.resourceControllerService.NewCreateResourceKeyOptions(
        keyName,
        serviceGUID,
    )
    createResourceKeyOptions.SetRole("Manager")

    resourceKey, response, err := c.resourceControllerService.CreateResourceKey(createResourceKeyOptions)
    if err != nil {
        return nil, errors.Wrap(err, "failed to create resource key")
    }
    if response.StatusCode != 201 {
        return nil, errors.New(fmt.Sprint("Failed creating resource key with error code: %i",
            response.StatusCode))
    }
    if resourceKey == nil {
        return nil, errors.New("Resource key is null")
    }

    return resourceKey, nil
}

// DeleteResourceKey Delete a resource key
func (c *ibmCloudClient) DeleteResourceKey(resourceKeyID string) error {
    deleteResourceKeyOptions := c.resourceControllerService.NewDeleteResourceKeyOptions(
        resourceKeyID,
    )

    response, err := c.resourceControllerService.DeleteResourceKey(deleteResourceKeyOptions)
    if err != nil {
        return errors.Wrapf(err, "failed to delete resource key with ID (%s)", resourceKeyID)
    }
    if response.StatusCode != 204 {
        return fmt.Errorf("Failed to delete resource key (%s) with error code: %d",
            resourceKeyID, response.StatusCode)
    }
    return nil
}

// initResourceController Get handle to the resource controller service
// for a particular user as specified by user API key.
func initResourceController(userAPIKey string) (*resourcecontrollerv2.ResourceControllerV2, error) {
    // Create an IAM authenticator
    authenticator := &core.IamAuthenticator{
        ApiKey: userAPIKey,
    }

    // Create a service options struct
    options := &resourcecontrollerv2.ResourceControllerV2Options{
        Authenticator: authenticator,
        URL:           "https://resource-controller.cloud.ibm.com",
    }

    // Construct the service client
    resourceControllerService, err := resourcecontrollerv2.NewResourceControllerV2(options)
    if err != nil {
        return nil, errors.Wrap(err, "failed to init resource contoller")
    }

    return resourceControllerService, nil
}

This wrapper function simplifies the Reconcile code when it creates the resource key as follows:

icClient, err := ibmcloudclient.NewClient(instance.Spec.UserAPIKey)
if err != nil {
    logger.Error(err, "Resource key creation failure")
    // Error creating resource key. Wait until it is fixed.
    return r.requeueOnErr(err)
}
resourceKey, err := icClient.CreateResourceKeyForServiceInstance(instance.Spec.ServiceGUID)
if err != nil {
    logger.Error(err, "Resource key creation failure")
    // Error creating resource key. Wait until it is fixed.
    return r.requeueOnErr(err)
}

And when the function deletes the previous key:

err := icClient.DeleteResourceKey(deleteResourceKeyID)
if err != nil {
    logger.Error(err, "Resource key deletion failure")
    // Error deleting resource key. Wait until it is fixed.
    return r.requeueOnErr(err)
}

Note: While this example created the resource directly in the IBM Cloud, since that is where our Cloudant database is deployed, in other situations you may need to create the resource in an external system. For example, the back end might be a remote SaaS CRM system. In this case, you would replace the previous logic with whatever is necessary to create the new resource in that external back-end system.

3. Resource key

The resource key is stored in the cluster in a Secret object. The k8s.io/api/core/v1 package of the Go client library is used to create and update the Secret object.

The Secret object is constructed in the newSecretObject helper function as follows:

func newSecretObject(resourceKeyID, resourceKeyAPIKey, serviceURL, namespace string) *corev1.Secret {
    // apply labels
    var lbs = make(map[string]string)
    lbs["name"] = "cloudant"
    lbs["owner"] = "credential-rotator-controller"

    var immutable bool = false

    // create and return secret object.
    return &corev1.Secret{
        ObjectMeta: metav1.ObjectMeta{
            Name:      "cloudant",
            Namespace: namespace,
            Labels:    lbs,
        },
        Type: "example/credential-rotator-controller",
        Data: map[string][]byte{"url": []byte(serviceURL), "iamApiKey": []byte(resourceKeyAPIKey),
            "resourceKeyID": []byte(resourceKeyID)},
        Immutable: &immutable,
    }
}

Note: The Secret object is set to immutable = false. This enables the Secret object to be updated with a new resource key. The object is immutable by default.

The newSecretObject helper function sets the following attributes on the object:

  • URL of the Cloudant database service. This is set as data in the Secret.
  • IAM API key of the resource key. This is set as data in the Secret.
  • Resource key ID, which is used later when the key is deleted after being replaced by a new key. This is set as data in the Secret.
  • Some metadata to identify the Secret object.

The Secret object is updated in the updateSecretObject helper function as follows:

func updateSecretObject(secret *corev1.Secret, resourceKey *resourcecontrollerv2.ResourceKey) {
    secret.Data["iamApiKey"] = []byte(*resourceKey.Credentials.Apikey)
    secret.Data["resourceKeyID"] = []byte(*resourceKey.ID)
    secret.ObjectMeta.Labels["modifiedAt"] = strconv.Itoa(int(time.Now().Unix()))
}

This updates the data fields with the new resource key details and also changes the modification timestamp in the Labels field.

The creation or update of the Secret object is handled in the CREATING phase of the Reconcile function by using the helper function as follows:

// If secret exists, first get the resource key ID and set it
// to 'instance.Status.PreviousResourceKeyID' so that the
// previous key can be removed in 'DELETING' phase. Then
// update the secret with new key created in 'CREATING' phase.
// Otherwise create secret adding the resource key created.
secret := newSecretObject(*resourceKey.ID, *resourceKey.Credentials.Apikey, instance.Spec.ServiceURL, instance.Spec.AppNameSpace)
found := &corev1.Secret{}
err = r.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, found)
if err != nil && errors.IsNotFound(err) { //Create secret
    err = r.Create(ctx, secret)
    if err != nil {
        // Error creating secret. Wait until it is fixed.
        return r.requeueOnErr(err)
    }
    logger.Info("Secret created", "Name", "Namespace", secret.Name, secret.Namespace)
} else if err != nil {
    // Error getting secret. Wait until it is fixed.
    return r.requeueOnErr(err)
} else { //Update secret
    instance.Status.PreviousResourceKeyID = string(found.Data["resourceKeyID"])
    updateSecretObject(found, resourceKey)
    err = r.Update(ctx, found)
    if err != nil {
        // Error updating secret. Wait until it is fixed.
        return r.requeueOnErr(err)
    }
    logger.Info("Secret updated", "Name", "Namespace", secret.Name, secret.Namespace)
}

The previous code snippet shows that the Secret object is initialized with the newSecretObject helper function. It then uses the controller-runtime Get function to check if the Secret object already exists in the cluster. If not, it calls the controller-runtime Create function to create it in the cluster. If the object exists, it calls the updateSecretObject helper function first and then the controller-runtime Update function.

4. Web application

You can restart the web application instances by updating the web application’s Deployment object, which forces a controlled and synchronized start of new Pods and a termination of the old Pods. This ensures no downtime during the restart.

The following code listing performs the restart and is called during the NOTIFYING phase:

found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: instance.Spec.AppName, Namespace: instance.Spec.AppNameSpace}, found)
if err != nil && errors.IsNotFound(err) {
    logger.Info("No Deployment found", "Deployment", instance.Spec.AppName,
        "Namespace", instance.Spec.AppNameSpace)
} else if err != nil {
    return r.requeueOnErr(err)
} else { //Restart
    patch := []byte(fmt.Sprintf(`{"spec":{"template":{"metadata":{"labels":{"credentials-rotator-redeloyed":"%v"}}}}}`, time.Now().Unix()))
    err = r.Patch(ctx, found, client.RawPatch(types.StrategicMergePatchType, patch))
    if err != nil {
        // Error updating deployment. Wait until it is fixed.
        return r.requeueOnErr(err)
    }
    logger.Info("Deployment updated and restarted", "Deployment", instance.Spec.AppName,
        "Namespace", instance.Spec.AppNameSpace)
}

The k8s.io/api/apps/v1 package of the Go client library is used for managing Deployment objects and the controller-runtime Patch function updates an object.

5. Return types

The Reconcile function can produce various return types.

The function definition is Reconcile(ctx context.Context, req ctrl.Request) (Result, error).

The Reconcile function returns a (Result, err).

Let’s first focus on the Result struct, which has two fields: Requeue and RequeueAfter.

  • Requeue is a Boolean data type that tells the Reconcile function to queue again. This data type defaults to false.
  • RequeueAfter expects a time.Duration that tells the reconciler to requeue after a specific amount of time.

For example, the following code requeues after 30 seconds.

return ctrl.Result{RequeueAfter: 30 * time.Second}, nil

Furthermore, the controller requeues the request again if the error is not nil, or Result.Requeue is true.

Most common return types

Three of the most common return types are:

  1. return ctrl.Result{Requeue: true}, nil, which often occurs when the state of the cluster or spec is updated. This type returns and requeues the request.
  2. return ctrl.Result{}, err, which occurs when there is an error and requeues the request.
  3. return ctrl.Result{}, nil, which occurs when the function is successful and the function doesn’t need to requeue. This type occurs at the bottom of the Reconcile loop, when the observed state of the cluster matches the desired state.

The Credential Rotator Operator uses all three of the common types and wraps them in helper functions as follows:

// doNotRequeue Finished processing. No need to put back on the reconcile queue.
func (r *CredentialRotatorReconciler) doNotRequeue() (reconcile.Result, error) {
    return ctrl.Result{}, nil
}

// requeue Not finished processing. Put back on reconcile queue and continue.
func (r *CredentialRotatorReconciler) requeue() (reconcile.Result, error) {
    return ctrl.Result{Requeue: true}, nil
}

// requeueOnErr Failed while processing. Put back on reconcile queue and try again.
func (r *CredentialRotatorReconciler) requeueOnErr(err error) (reconcile.Result, error) {
    return ctrl.Result{}, err
}

The Reconcile function processes the CR request in a phased approach. After a phase is completed successfully, the next phase is set on the CR status and the CR instance status is updated. The CR instance is then requeued to be processed again in the next cycle.

// Update the CredentialRotator instance, setting the status to the respective phase
err = r.Status().Update(ctx, instance)
if err != nil {
    return r.requeueOnErr(err)
}

return r.requeue()

This continues until the DONE phase is reached, where the CR instance is now fully processed. The CR instance is no longer requeued and the reconcile request is released.

case securityv1alpha1.PhaseDone:
    logger.Info("Phase: DONE")
    r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhaseDone)
    return r.doNotRequeue()

requeueOnErr is used if there is an error during the state handling and the process requeues. For example:

resourceKey, err := icClient.CreateResourceKeyForServiceInstance(instance.Spec.ServiceGUID)
if err != nil {
    logger.Error(err, "Resource key creation failure")
    // Error creating resource key. Wait until it is fixed.
    return r.requeueOnErr(err)
}

6. Kubernetes events

Kubernetes events are used to show what is happening within a cluster. The Credential Rotator Operator controller generates an event at the start of each phase to show what phase is currently in progress. For example, the CREATING phase generates an event of type Normal, reason PhaseChange, and a message of CREATING as follows:

case securityv1alpha1.PhaseCreating:
    logger.Info("Phase: CREATING")
    r.Recorder.Event(instance, "Normal", "PhaseChange", securityv1alpha1.PhaseCreating)
    [..]

This means that if a user checks the progress of the CR request by checking its details, the user will see the events generated by the controller:

$ kubectl describe credentialrotators.security.example.com credentialrotator-demo

Name:         credentialrotator-demo
[...]
Events:
  Type    Reason       Age                 From                    Message
  ----    ------       ----                ----                    -------
  Normal  PhaseChange  89s                 credential-rotator      PENDING
  Normal  PhaseChange  16s (x13 over 89s)  credential-rotator      CREATING

The k8s.io/client-go/tools package of the Go client library is used to construct the event and put it in the queue for sending. The EventRecorder interface (named Recorder in this controller), which is used to generate events, is initialized during the controller initialization in the entry point of the application (main.go):

if err = (&controllers.CredentialRotatorReconciler{
    Client:   mgr.GetClient(),
    Scheme:   mgr.GetScheme(),
    Recorder: mgr.GetEventRecorderFor("credential-rotator"),
}).SetupWithManager(mgr); err != nil {
    setupLog.Error(err, "unable to create controller", "controller", "CredentialRotator")
    os.Exit(1)
}

7. KubeBuilder markers

So, what are the KubeBuilder markers that are in the previous Reconcile function?

//+kubebuilder:rbac:groups=security.example.com,resources=credentialrotators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=security.example.com,resources=credentialrotators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=security.example.com,resources=credentialrotators/finalizers,verbs=update

// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=secrets/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=events/status,verbs=get;update;patch

KubeBuilder markers are single-line comments that start with a plus symbol, followed by a marker name to enable config and code generation.

These markers are extremely important, especially when used for RBAC. The controller-gen utility, which is listed in your bin directory, is what actually generates code and YAML files from these markers.

//+kubebuilder:rbac:groups=security.example.com,resources=credentialrotators,verbs=get;list;watch;create;update;patch;delete

This marker generates RBAC which enables the operator to get, list, watch, create, update, path, and delete credentialrotators resources within the security.example.com API Group.

If you run make manifests, the controller-gen utility sees the new KubeBuilder marker and updates the RBAC YAML files in the config/rbac directory to change the RBAC configuration.

For example, if your credentialrotator resource didn’t have the Update verb listed in the KubeBuilder marker, you would not be able to use r.Update() on our credentialrotator resource. Instead, you would get a permissions error such as Failed to update *security.example.com.CredentialRotator. If you change these markers and add the list command, you must run make manifests to apply the changes from your KubeBuilder commands into your config/rbac YAML files.

You can learn more about KubeBuilder markers in the KubeBuilder docs.

Conclusion

This article explained the underlying logic of the custom controller code of the operator solution mentioned in the previous article of this learning path. Hopefully, you now have a better understanding of how to:

  • Use the Kubernetes Go client Reader and Writer interface to Get, Create, and Update resources.
  • Use the StatusWriter interface to update the status of a subresource, for example, the current state.
  • Automate the rotation of Cloudant service credentials, ensure credentials are stored in the cluster, and ensure web app instances that use the service are restarted.
  • Use the EventRecorder interface to generate events.
  • Use KubeBuilder markers to change RBAC policies and apply those policies to your CR.

Next steps

Next, we deploy this operator and see it in action!