---
title: "Safety gates: dry runs, approvals and pruning"
description: "How monedula-gitops gates risky changes: dry runs, --allow-destructive, --allow-delete and --prune approvals, and operator deletion policies."
url: https://monedula.dev/flock/docs/gitops/concepts/safety-gates/
---

Every change can be previewed: [`validate`](/flock/docs/gitops/reference/cli/validate/) checks manifests without a running Kafka, and [`diff`](/flock/docs/gitops/reference/cli/diff/) and [`apply --dry-run`](/flock/docs/gitops/reference/cli/apply/) list the planned operations without changing anything. `KafkaUser` passwords are resolved only when an operation executes, never at plan time, so they never appear in a rendered plan.

## Operations that need approval

Three kinds of operation run only with explicit approval. The CLI approves with flags on `apply`, the operator with annotations and fields on the resource:

| Gate | Covers | Without approval | CLI | Operator |
| --- | --- | --- | --- | --- |
| Destructive | a partition increase, a replication factor change, a schema compatibility lowering (including a first-time level below the registry's global default), a quota limit removal | reported as `Blocked` and not run; the rest of the plan runs | `--allow-destructive` | annotation `gitops.monedula.dev/allow-destructive: "true"` |
| Delete | Kafka cleanup when a `KafkaTopic` or `KafkaAccessPolicy` under `deletionPolicy: Delete` is deleted | finalizer kept, `DeleteNotApproved` Warning event | `--allow-delete` exists but has nothing to approve: the CLI never plans a deletion | annotation `gitops.monedula.dev/allow-delete: "true"` |
| Prune | in-scope ACLs and role bindings that the manifests no longer declare | reported as `PruneDisabled` and kept | `--prune` | `spec.prune: true` |

Each gate is separate: `--allow-destructive` does not enable pruning, and the prune opt-in approves nothing else.

In the CLI, when `Blocked` operations are the only problem, `apply` exits 3: the plan is sound and waits for approval. In the operator, a blocked operation leaves the resource at `Ready=False`, and the operator emits an event.

Two changes never run, approved or not. A partition decrease is always rejected, because Kafka cannot shrink a topic. A replication factor change passes the gate but is then reported as `Unsupported`, because monedula-gitops cannot perform the partition reassignment it needs.

## Pruning is opt-in

Pruning deletes in-scope ACLs and Confluent role bindings that are no longer desired, for example after you remove an operation from an `access` entry that still names the principal. Removing the whole entry takes that principal and topic pair out of the managed scope, so its ACLs are neither pruned nor reported. Pruning is off by default so that an accidentally truncated manifest cannot cut production access:

- Without the opt-in, a prune candidate is reported as `PruneDisabled` by `diff`, `verify` and `apply`. `verify` counts it as drift, but it is never deleted and it does not fail `apply`.
- In the operator, a candidate is deleted only when every resource whose scope covers it has `spec.prune: true`; otherwise it stays as `PruneDisabled` drift.

Pruning never touches ACLs outside the managed scope.

<Callout type="warning" title="Managed scope is per principal and resource pattern">
If a manifest declares access for `User:svc-checkout` on the topic `orders`, every live ACL for that pair is in scope, including grants made by hand before the manifest existed. A hand-made grant for that pair becomes a prune candidate. Bring such grants into your manifests, or use a different principal for access that must stay outside GitOps. Keep each pair declared in exactly one pipeline, or two pipelines that both prune will delete each other's grants. Role bindings are scoped per principal, role and MDS scope, on any resource.
</Callout>

[Delete resources safely with `--prune`](/flock/docs/gitops/how-to/delete-resources-safely/) walks through a prune and lists what it leaves in place.

## Deletion policies in the operator

When a custom resource is deleted, its finalizer, `gitops.monedula.dev/finalizer`, follows `spec.deletionPolicy`: `Orphan` leaves the Kafka objects in place, and `Delete` removes them.

| Kind | Default | `Delete` removes | Needs `allow-delete` |
| --- | --- | --- | --- |
| `KafkaTopic` | `Orphan` | the topic's ACLs, then the topic, then the schema subjects whose content the topic manages | yes |
| `KafkaAccessPolicy` | `Delete` | the policy's managed ACLs | yes |
| `KafkaRoleBinding` | `Delete` | its MDS role bindings | no |
| `KafkaUser` | `Delete` | its SCRAM credential | no |

`Delete` keeps every ACL and role binding that another live resource still declares. The operator then emits a `SharedACLsRetained` or `SharedRoleBindingsRetained` Normal event that names up to three of those resources as `Kind/namespace/name` and counts the rest as "and N more".

Role bindings and users need no annotation because their external state is their whole reason to exist: deleting the resource is itself the explicit action. A `KafkaQuota` has no deletion policy, and deleting it removes its quota.

If the operator cannot build a client for the cluster, because the `KafkaCluster` is gone or its credentials cannot be read, it keeps the finalizer and emits a `DeletionBlocked` Warning event. The annotation `gitops.monedula.dev/force-finalizer-removal: "true"` then removes the finalizer without cluster-side cleanup. When the `KafkaCluster` exists but Kafka is down or rejects the cleanup, that annotation is not consulted: a `KafkaTopic` or `KafkaAccessPolicy` retries with a `DeleteFailed` Warning event, and a `KafkaRoleBinding` with `DeletionFailed`, until the cleanup succeeds or you switch its `deletionPolicy` to `Orphan`. A `KafkaQuota` or `KafkaUser` emits a `QuotaDeletionFailed` or `CredentialDeletionFailed` Warning event and finishes the deletion anyway.

## Execution never rolls back

`apply` is best-effort: independent operations continue past a failure, ACLs for a topic that failed to be created are skipped, and the next `apply` converges the rest. The [CLI overview](/flock/docs/gitops/reference/cli/) lists every operation outcome and exit code.

## Related

- [Delete topics from Kubernetes](/flock/docs/gitops/how-to/delete-topics-from-kubernetes/) applies the deletion policies to a topic.
- [Drift and reconciliation modes](/flock/docs/gitops/concepts/drift-and-reconciliation/) explains what counts as drift.