Troubleshooting
This page collects the most common problems when running a KubeOps operator, their causes, and where to find the relevant configuration.
Reconciliation does not fire
Symptom: You change a resource, but ReconcileAsync is never called.
The most common causes, in order of likelihood:
- The change does not increment
metadata.generation. With the default Reconcile Strategy (ByGeneration), label and annotation changes never trigger reconciliation, and status updates don't either when the CRD has a status subresource (the default for KubeOps entities). Switch toByResourceVersiononly if your controller must react to every write. - The event was deduplicated. The watcher caches the last seen generation/resourceVersion per object — see Caching for exactly which events are skipped.
- The resource is outside the watched scope. Check
OperatorSettings.Namespaceand any label or field selectors on the controller — the API server only delivers matching objects. - The controller is not registered. Verify
RegisterComponents()(or an explicitAddController<,>()) is called. Enable Registration Validation to fail fast at startup on wiring gaps.
Reconciliation fires in an endless loop
Symptom: The same entity is reconciled over and over without external changes.
- With
ByResourceVersion, every write your controller performs (including status updates) produces a new watch event. Guard all writes: only update when the value actually changed — see the infinite-loop warning. - A
RequeueAfterreturned unconditionally creates a periodic loop by design; make sure that is intended. - Updating the spec inside
ReconcileAsyncre-triggers reconciliation even underByGeneration(spec changes increment the generation). Reconcilers should write status, not spec.
HTTP 409 Conflict on updates
Symptom: UpdateAsync/UpdateStatusAsync fails with Conflict.
Every mutating call returns the entity with a new resourceVersion. Reusing a stale reference
for a subsequent call fails with 409. Always continue working with the entity instance returned by
the client:
entity = await client.UpdateStatusAsync(entity, cancellationToken); // use the returned entity
409s can also come from another actor (or a second operator instance without leader election) writing the same object concurrently. Treat them as retryable: re-fetch, re-apply, or simply let the failed reconciliation retry.
Entity is stuck in "Terminating"
Symptom: A resource has a DeletionTimestamp but is never deleted.
Kubernetes only deletes an object once its finalizer list is empty. The object stays in
Terminating when:
- a registered finalizer keeps failing or never returns success — check the operator logs for the finalizer's errors;
- the operator is not running (nothing processes the finalizer); or
- a finalizer identifier is left over from a previous operator version that no longer registers it.
As a last resort in development, remove the finalizer manually:
kubectl patch <resource> <name> --type merge -p '{"metadata":{"finalizers":null}}'
Manually removing finalizers skips your cleanup logic. Never do this routinely in production.
403 Forbidden / RBAC errors
Symptom: The operator logs Forbidden errors, watches never start, or leader election fails.
The operator's service account is missing permissions. Declare what your operator needs with the
RBAC attributes, regenerate the installation manifests (see
CLI), and re-apply them. Leader election additionally needs the leases permissions from
the default RBAC rules. Note that RBAC problems rarely show up during
local development, where you typically run with an admin kubeconfig.
Webhooks are not called or block resource operations
Symptom: Admission requests fail, or creates/updates hang and fail with a webhook timeout.
- The API server requires TLS for webhook endpoints — a webhook served over plain HTTP is never called. See Local Development Considerations for certificate options during development.
- A webhook configuration with
FailurePolicy: Failblocks resource operations while the webhook is unreachable. Decide consciously betweenFailandIgnore— customizable via the webhook configuration factory. - Check that the webhook
Service/URL in the generated configuration actually points at your operator and that the CA bundle matches the serving certificate.
Watcher keeps reconnecting / 410 Gone in logs
Symptom: Log messages about the watcher reconnecting or restarting with a reset bookmark.
This is usually normal behavior: the Kubernetes API server terminates long-running watch
connections periodically, and 410 Gone simply means the cached resource version expired — the
watcher re-lists and continues. KubeOps reconnects automatically with exponential backoff.
Investigate only if the operator makes no progress between reconnects; then check API server
availability, network policies, and RBAC watch permissions.
CRDs are missing in the cluster
Symptom: The operator starts, but watching fails because the CRD kind is unknown.
Install the CRDs before or with the operator: generate them with the CLI
(kubeops generate operator), apply them as part of the deployment, or — for
development only — use the CRD Installer.
RegisterComponents() is not found (CS1061)
Symptom: The build fails with 'IOperatorBuilder' does not contain a definition for 'RegisterComponents'.
Make sure the KubeOps.Generator package is referenced and the project compiles otherwise —
source generators do not run on broken compilations. If the KubeOpsGeneratorNamespace MSBuild
property is set, the generated classes live in that namespace instead of the global namespace;
top-level Program.cs files then need a matching using directive (e.g. using MyOperator;).
Components of a referenced library are not registered / warning KOG002
Symptom: Controllers or finalizers that live in a referenced class library never reconcile,
and the build reports KOG002.
Two assemblies in the reference chain generate registration classes with the same fully qualified
name (by default, the global namespace is used). The generator skips the conflicting registration
instead of emitting ambiguous code. Set the KubeOpsGeneratorNamespace MSBuild property (e.g. to
$(RootNamespace)) in the conflicting projects so each assembly generates into its own
namespace — see KubeOps.Generator.
Leader election issues
See Leader Election — Troubleshooting for lease permissions, timing settings, and diagnosing leadership transitions.
Still stuck?
- Raise the log level for the
KubeOpscategory toTrace(see Logging) — the watcher and queue log every decision, including why an event was skipped. - Check the metrics (
kubeops.operator.queue.depth,kubeops.operator.reconciliation, watcher events) to see where the pipeline stops. - Search the GitHub issues or ask on the Discord.