Using Finalizers

Finalizers allow controllers to implement asynchronous pre-delete hooks. Let’ssay you create an external resource (such as a storage bucket) for each object ofyour API type, and you want to delete the associated external resourceon object’s deletion from Kubernetes, you can use a finalizer to do that.

You can read more about the finalizers in the Kubernetes reference docs. The section below demonstrates how to register and trigger pre-delete hooksin the Reconcile method of a controller.

The key point to note is that a finalizer causes “delete” on the object to becomean “update” to set deletion timestamp. Presence of deletion timestamp on the objectindicates that it is being deleted. Otherwise, without finalizers, a deleteshows up as a reconcile where the object is missing from the cache.

Highlights:

  • If the object is not being deleted and does not have the finalizer registered,then add the finalizer and update the object in Kubernetes.
  • If object is being deleted and the finalizer is still present in finalizers list,then execute the pre-delete logic and remove the finalizer and update theobject.
  • Ensure that the pre-delete logic is idempotent.

Apache License

Licensed under the Apache License, Version 2.0 (the “License”);you may not use this file except in compliance with the License.You may obtain a copy of the License at

  1. http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an “AS IS” BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License. ImportsFirst, we start out with some standard imports.As before, we need the core controller-runtime library, as well asthe client package, and the package for our API types.

  1. package controllers
  2. import (
  3. "context"
  4. "github.com/go-logr/logr"
  5. ctrl "sigs.k8s.io/controller-runtime"
  6. "sigs.k8s.io/controller-runtime/pkg/client"
  7. batchv1 "tutorial.kubebuilder.io/project/api/v1"
  8. )

The code snippet below shows skeleton code for implementing a finalizer.

  1. func (r *CronJobReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
  2. ctx := context.Background()
  3. log := r.Log.WithValues("cronjob", req.NamespacedName)
  4. var cronJob batch.CronJob
  5. if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
  6. log.Error(err, "unable to fetch CronJob")
  7. // we'll ignore not-found errors, since they can't be fixed by an immediate
  8. // requeue (we'll need to wait for a new notification), and we can get them
  9. // on deleted requests.
  10. return ctrl.Result{}, ignoreNotFound(err)
  11. }
  12. // name of our custom finalizer
  13. myFinalizerName := "storage.finalizers.tutorial.kubebuilder.io"
  14. // examine DeletionTimestamp to determine if object is under deletion
  15. if cronJob.ObjectMeta.DeletionTimestamp.IsZero() {
  16. // The object is not being deleted, so if it does not have our finalizer,
  17. // then lets add the finalizer and update the object. This is equivalent
  18. // registering our finalizer.
  19. if !containsString(cronJob.ObjectMeta.Finalizers, myFinalizerName) {
  20. cronJob.ObjectMeta.Finalizers = append(cronJob.ObjectMeta.Finalizers, myFinalizerName)
  21. if err := r.Update(context.Background(), cronJob); err != nil {
  22. return ctrl.Result{}, err
  23. }
  24. }
  25. } else {
  26. // The object is being deleted
  27. if containsString(cronJob.ObjectMeta.Finalizers, myFinalizerName) {
  28. // our finalizer is present, so lets handle any external dependency
  29. if err := r.deleteExternalResources(cronJob); err != nil {
  30. // if fail to delete the external dependency here, return with error
  31. // so that it can be retried
  32. return ctrl.Result{}, err
  33. }
  34. // remove our finalizer from the list and update it.
  35. cronJob.ObjectMeta.Finalizers = removeString(cronJob.ObjectMeta.Finalizers, myFinalizerName)
  36. if err := r.Update(context.Background(), cronJob); err != nil {
  37. return ctrl.Result{}, err
  38. }
  39. }
  40. return ctrl.Result{}, err
  41. }
  42. // rest of the reconciler code
  43. }
  44. func (r *Reconciler) deleteExternalResources(cronJob *batch.CronJob) error {
  45. //
  46. // delete any external resources associated with the cronJob
  47. //
  48. // Ensure that delete implementation is idempotent and safe to invoke
  49. // multiple types for same object.
  50. }
  51. // Helper functions to check and remove string from a slice of strings.
  52. func containsString(slice []string, s string) bool {
  53. for _, item := range slice {
  54. if item == s {
  55. return true
  56. }
  57. }
  58. return false
  59. }
  60. func removeString(slice []string, s string) (result []string) {
  61. for _, item := range slice {
  62. if item == s {
  63. continue
  64. }
  65. result = append(result, item)
  66. }
  67. return
  68. }