> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Demo Add A Finalizer To WebApp

> Explains adding and using a Kubernetes finalizer in a WebApp operator controller to ensure cleanup before resource deletion, demonstrating reconcile patterns and controllerutil helper usage.

By default the WebApp delete flow in this operator relies solely on [owner references](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/). That works for in-cluster resources like Deployments, Services, and ConfigMaps because the [Kubernetes garbage collector](https://kubernetes.io/docs/concepts/architecture/garbage-collection/) will remove dependent objects automatically when their owner is deleted.

However, some cleanup tasks cannot be inferred by Kubernetes — for example, cleaning up external cloud resources, revoking credentials, or performing any operator-specific final work. For those cases you need a [finalizer](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/) on the custom resource so the controller can run cleanup before the API server actually deletes the object.

The problem with the current behavior (before adding a finalizer) is demonstrated here: creating the WebApp and immediately deleting it completes quickly because nothing blocks the deletion.

```bash theme={null}
$ kubectl apply -f ../webapp-site.yaml
namespace/webapp-demo created
webapp.webapp.kodekloud.com/site created

$ kubectl -n webapp-demo delete webapp site
webapp.webapp.kodekloud.com "site" deleted from webapp-demo namespace
```

That fast disappearance prevents the controller from performing any needed cleanup. The solution is to add a finalizer string owned by the operator and implement the standard finalizer pattern in the controller's Reconcile logic.

<Callout icon="lightbulb" color="#1CB2FE">
  [controller-runtime helpers](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil) simplify adding, checking, and removing finalizers: each controller should only add and remove its own finalizer string. Using these helpers avoids manual metadata slice manipulation.
</Callout>

## Finalizer name and placement

Use a unique, domain-style finalizer string and declare it as a package-level constant so the controller uses a single canonical value everywhere.

```go theme={null}
import (
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	webappv1 "github.com/kodekloud/webapp-operator/api/v1"
)

const webAppFinalizer = "webapp.kodekloud.com/finalizer"
```

For reference, the controller struct looks like:

```go theme={null}
type WebAppReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder record.EventRecorder
}
```

## The finalizer pattern in Reconcile

A well-structured Reconcile should clearly separate two paths:

1. Normal (non-deleting) path — ensure the finalizer is present.
2. Deletion path — perform cleanup and remove the finalizer so Kubernetes can finish deletion.

Key points:

* Only add the finalizer when the object is not being deleted (DeletionTimestamp == zero).
* When adding the finalizer, persist the object with `r.Update` and then requeue so the next reconcile will operate on the object with the updated `resourceVersion`.
* When deletion is in progress (DeletionTimestamp non-nil), only perform cleanup if your finalizer is present; then remove it and `Update` so the API server can complete deletion.

### Add finalizer during normal reconciliation

Check the DeletionTimestamp and add the finalizer if missing. Use `controllerutil.ContainsFinalizer` and `controllerutil.AddFinalizer` to avoid unnecessary updates.

```go theme={null}
if webapp.DeletionTimestamp.IsZero() {
    if !controllerutil.ContainsFinalizer(&webapp, webAppFinalizer) {
        controllerutil.AddFinalizer(&webapp, webAppFinalizer)
        if err := r.Update(ctx, &webapp); err != nil {
            return ctrl.Result{}, err
        }
        // Requeue to fetch the object with the updated finalizer/resourceVersion
        return ctrl.Result{Requeue: true}, nil
    }
    // Continue normal reconciliation when finalizer already present
}
```

### Deletion branch — perform cleanup, then remove finalizer

When the Resource's DeletionTimestamp is set the API server has accepted a delete request but is holding the object until all finalizers are removed. Act only if your finalizer is present; perform any required controller-specific cleanup, then remove your finalizer and `Update` the object so Kubernetes can finish deletion.

A full Reconcile example that demonstrates both branches:

```go theme={null}
package controllers

import (
    "context"

    appsv1 "k8s.io/api/apps/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

    webappv1 "github.com/kodekloud/webapp-operator/api/v1"
)

const webAppFinalizer = "webapp.kodekloud.com/finalizer"

func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var webapp webappv1.WebApp
    if err := r.Get(ctx, req.NamespacedName, &webapp); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // 1) Normal path: object not being deleted -> ensure finalizer exists
    if webapp.DeletionTimestamp.IsZero() {
        if !controllerutil.ContainsFinalizer(&webapp, webAppFinalizer) {
            controllerutil.AddFinalizer(&webapp, webAppFinalizer)
            if err := r.Update(ctx, &webapp); err != nil {
                return ctrl.Result{}, err
            }
            // Requeue so we operate on the updated object next pass
            return ctrl.Result{Requeue: true}, nil
        }
        // Continue with normal reconciliation (omitted here)
    }

    // 2) Deletion path: object is being deleted
    if !webapp.DeletionTimestamp.IsZero() {
        // Only act if our finalizer is present
        if controllerutil.ContainsFinalizer(&webapp, webAppFinalizer) {
            // Example placeholder: build a Deployment object reference or perform other cleanup
            dep := &appsv1.Deployment{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      webapp.Name,
                    Namespace: webapp.Namespace,
                },
            }

            // Perform cleanup here (e.g., delete external resources, revoke cloud resources)
            // In this demo, we'll log and consider cleanup done.
            ctrl.LoggerFrom(ctx).Info("running cleanup for WebApp", "name", webapp.Name)

            // Remove our finalizer and persist the object so Kubernetes can complete deletion
            controllerutil.RemoveFinalizer(&webapp, webAppFinalizer)
            if err := r.Update(ctx, &webapp); err != nil {
                return ctrl.Result{}, err
            }
            // After removing finalizer and successful update, the object will be deleted by the API server.
            return ctrl.Result{}, nil
        }
        // If our finalizer is not present, nothing for us to do; let deletion proceed.
    }

    return ctrl.Result{}, nil
}
```

<Callout icon="warning" color="#FF6B6B">
  Do not remove finalizers that you do not own. Always check `ContainsFinalizer` before calling `RemoveFinalizer` to avoid interfering with other controllers’ cleanup. Also return errors from `Update` so controller-runtime will retry on transient failures.
</Callout>

## Verify deletion completed

Once the controller has removed its finalizer and the API server finishes deletion, a subsequent get should return NotFound:

```bash theme={null}
$ kubectl -n webapp-demo get webapp site
Error from server (NotFound): webapps.webapp.kodekloud.com "site" not found
```

A `NotFound` response indicates the delete request completed successfully and the finalizer flow worked: your controller performed cleanup and released its finalizer so Kubernetes could complete deletion.

## Quick summary and checklist

| Step                       | Purpose                                                     | Example / Note                                                              |
| -------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- |
| Add finalizer              | Ensure controller can run cleanup before deletion           | Use `controllerutil.AddFinalizer` when `DeletionTimestamp.IsZero()`         |
| Persist change & requeue   | Let next reconcile operate on updated resource              | Call `r.Update(ctx, &webapp)` then `return ctrl.Result{Requeue: true}, nil` |
| On delete, check ownership | Only perform cleanup if your finalizer is present           | `controllerutil.ContainsFinalizer(&webapp, webAppFinalizer)`                |
| Perform cleanup            | Delete external resources or perform operator-specific work | Idempotent cleanup is recommended                                           |
| Remove finalizer & update  | Allow API server to finish deletion                         | `controllerutil.RemoveFinalizer` + `r.Update`                               |

## References

* [Kubernetes finalizers](https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/)
* [Owners and dependents (ownerReferences)](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/)
* [Kubernetes garbage collection](https://kubernetes.io/docs/concepts/architecture/garbage-collection/)
* [controller-runtime controllerutil helpers](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil)

<CardGroup>
  <Card title="Watch Video" icon="video" cta="Learn more" href="https://learn.kodekloud.com/user/courses/kubernetes-operators/module/6a375c4e-4bda-4d13-a58f-4d85961676cc/lesson/a26444ee-e070-4ef7-b784-86238646c77e" />

  <Card title="Practice Lab" icon="flask-conical" cta="Learn more" href="https://learn.kodekloud.com/user/courses/kubernetes-operators/module/6a375c4e-4bda-4d13-a58f-4d85961676cc/lesson/ba550e4e-3605-4f5a-9e99-715ab68812d8" />
</CardGroup>
