Skip to main content
Dorgu understands your whole cluster. It only changes what you have explicitly handed it. For a Deployment that Helm, ArgoCD, or Flux reconciles, Dorgu is understanding and recommendation only. It still detects the failure, still diagnoses the root cause, and still tells you exactly which value to change. It will not patch the Deployment. Instead it names the owner and shapes the fix as an instruction for that owner’s source of truth. Those three are named deliberately. All three run a controller and stamp what they reconcile, so their ownership is a fact Dorgu can read off the object. kustomize is not in that list, and the reason is worth reading before you rely on any of this: see the kustomize limitation.
Version requirement. The ownership record ships in operator v0.9.0; the refusal is enforced by CLI v0.10.0. Both halves are needed for the guard to hold, and neither has been superseded. Which versions to actually be on, and what every mismatch does: version coupling.
Two corrections land in operator v0.10.0 and CLI v0.11.0. Clean-room run #3 disproved two claims this page used to make, and both fixes now ship:
  • An Update-operation field manager does conflict with a later server-side apply. CLI v0.11.0 removes its own field-manager entry after patching, and operator v0.10.0 counts a foreign Update-op owner of a container’s resources as ownership.
  • The kustomize rule never fired on real kustomize output. Operator v0.10.0 matches the markers kustomize actually emits, and the limitation is stated plainly instead of being papered over.
Both are described in place below. Run the newest pair: on operator v0.9.0 and CLI v0.10.0, this page’s ownership guard works but a heal still leaves a field-manager entry behind.

Why Dorgu refuses

Patching an owned Deployment does not merely get reverted. It breaks the owner’s next deploy. Under server-side apply, the field manager that last set a field owns it. When the Dorgu CLI patches resources.limits.memory on a Deployment that Helm installed, that patch claims the field away from Helm’s field manager. The fix works, the pod recovers, and everything looks fine. Then someone runs helm upgrade and it hard-fails on a field-manager conflict, because Helm still believes it owns a field it no longer does. A fix that breaks your next deploy is not a fix. Clean-room testing hit exactly that failure: Dorgu healed a Helm-managed app, printed a green checkmark, and left the release un-upgradable. The failure mode differs by owner, and none of them are good:

spec.workloadRef

At proposal time the operator reads the live Deployment and records what it found on RemediationAction.spec.workloadRef:
workloadRef is also what grounds every number Dorgu states. The ApplicationPersona is a point-in-time import that drifts from the running Deployment, so quoting persona values as current reality is how Dorgu once claimed a 96Mi limit on a container that had 32Mi. Facts, blast-radius caps, and diffs all come from observedResources now.

How ownership is detected

The operator reads server-side-apply field managers plus labels and annotations, most specific owner first, because owners layer on top of each other. A Flux HelmRelease renders Helm’s own labels, so Flux has to win; an ArgoCD-managed Helm chart likewise carries Helm metadata that ArgoCD, not Helm, actually reconciles. Three details are worth knowing:
  • An Update-operation field manager counts as an owner when it holds the fields a fix would write. This page used to say the opposite: that the Update-operation entry kubectl patch and kubectl set leave behind “claims no ongoing ownership of the fields, so it says nothing about whether a future patch will conflict”. That was wrong, and it was reproduced twice against a real API server. Apply conflict detection is about who owns the field, not about how they came to own it:
    So a foreign manager holding a container’s resources reads as unknown whichever operation put it there. The check is scoped to resources on purpose: a manager that owns only spec.replicas (an autoscaler) or a pod-template annotation (a sidecar injector) is not in the way of a resource patch, and treating it as an owner would make Dorgu refuse to heal on most real clusters for no safety gain. An entry whose field set cannot be parsed is treated as owning, on the same house rule as unknown.
  • Your own kubectl is not an owner, and neither is Dorgu. Managers named kubectl-*, dorgu, and kube-controller-manager are skipped by that rule. A human with kubectl is the definition of unmanaged rather than a counter-example to it, and refusing there would leave you worse off than healing. A leftover kubectl-set claim is a genuine conflict waiting for the next server-side apply, and a heal clears it rather than adding to it: see Dorgu leaves no field manager behind.
  • A Flux HelmRelease reads as flux, not helm, because Flux is what reconciles it. Editing chart values without going through the Flux source would get reverted.
unknown is treated as owned. Where the Deployment cannot be resolved, or its server-side applier is not one Dorgu recognises, managedBy is unknown and Dorgu explains rather than writes. Absence of evidence that patching is safe is not evidence that it is, and nothing is patched on a guess.One consequence worth knowing: a workload the operator cannot resolve also loses its advisory kubectl commands, so a resolution failure costs plan usefulness as well as grounding.

The kustomize limitation

Dorgu cannot reliably tell that a Deployment came from a kustomize overlay. Rather than advertise a protection it does not have, it says so. kustomize is a client-side renderer with no controller. It builds YAML and hands it to kubectl; nothing of kustomize is left running in your cluster, and nothing reconciles its output until a human runs kubectl apply -k again. A Deployment created that way is indistinguishable at the API level from one created with kubectl apply -f: the same kubectl-client-side-apply field manager, no label, no annotation. So a plain kubectl apply -k reads as unmanaged, and Dorgu will patch it. The three markers in the detection table above are all set by the kustomization, not by kustomize:
The rule this replaces never fired on real kustomize output. Detection required app.kubernetes.io/managed-by: kustomize exactly, and the label kustomize generates is versioned: kustomize-v5.8.1, checked against the version shipped inside kubectl. So the exact match only ever matched a bare kustomize somebody had hand-written into commonLabels. In clean-room testing a plain kubectl apply -k was classified unmanaged, which is precisely what the published claim said could not happen. The match is now on the kustomize prefix so both forms are recognised, and the honest statement is the one above: an overlay that opts into no build metadata is invisible to Dorgu.
What that costs you is narrower than it sounds, and Dorgu names it at the moment it matters. Because nothing reconciles an overlay on its own, a patch is not fought: it survives until somebody re-runs kubectl apply -k, at which point the overlay’s value wins and the fix is reverted. There is no conflict and no broken deploy, because Dorgu leaves no field-manager entry behind. So immediately before it writes, the CLI prints the limit of the classification that got it there:
Mirror the change back into your overlay, exactly as you would into Helm values.

Dorgu leaves no field manager behind

We change it, we do not own it. On the one class of workload Dorgu will write to, it takes the fields it needs and then hands them back. A heal is two writes, not one:
  1. A strategic-merge patch under the field manager dorgu, rather than kubectl’s default kubectl-patch. Using its own name is what makes the entry distinguishable from a kubectl patch you ran, which Dorgu has no business deleting.
  2. A second patch removing that dorgu entry from metadata.managedFields, followed by a read-back to confirm it is gone.
The second write is the point, and it does more than avoid harm. An Update takes the fields it writes away from whoever held them, so a heal moves any pre-existing kubectl-set or kubectl-patch claim on those fields onto dorgu and then drops it. The fields end up owned by nobody, which is the state a server-side apply can claim without a conflict. A heal now clears a conflict that was already there rather than merely avoiding a new one. Bring a healed Deployment under Helm, ArgoCD, or Flux afterwards and that first apply succeeds.
Why not --server-side --field-manager=dorgu --force-conflicts? Because it is the worse of the two. Forcing the conflict works once and makes Dorgu a persistent Apply-operation owner of those fields, which is exactly what the next helm upgrade would then have to fight. Patch and release leaves nothing behind to fight.
If the entry cannot be removed, Dorgu tells you. The removal is a read-modify-write over state Dorgu does not own, so it carries the resourceVersion as a precondition, retries against fresh state on a conflict, and reads the object back afterwards. The read-back is not ceremony: the API server accepts a client-supplied managedFields list on some paths and recomputes it on others, so the only honest way to report “Dorgu owns nothing here” is to look.The workload is patched either way, so a failure here is not a failed heal. It is a future apply conflict, and it is printed as a warning naming the fields Dorgu now owns and the command to clear them:
That clears the whole managedFields list; every other manager reclaims its fields on its next apply. Leaving the footprint in silence was never an option.

What changes about the plan

For an owned Deployment, the operator rewrites every step whose command would write to the cluster. The command is dropped, the description becomes what to change at the source, and the step’s rationale gains one line on what a direct patch would have broken.
The chart values key is hedged deliberately. Dorgu has not read your chart, so it says “commonly under resources” rather than asserting a path it cannot verify.

Read-only commands survive

On an owned workload, a step’s Run: line is kept when the command only reads:
kubectl logs, kubectl get events, kubectl describe, kubectl top, kubectl rollout status and the rest of the read-only verbs are printed unchanged, because reading matters most on exactly the workloads Dorgu will not patch. It is what is left to hand over. Classification is positive, not inferred: read-only verbs are listed explicitly, and anything unrecognised is refused rather than assumed harmless. That includes a bare kubectl rollout with no subcommand. The verb is found by scanning for the first token that is a known kubectl subcommand, so kubectl -n apps patch ... cannot hide patch behind a flag argument.
The operator strips workload-writing commands before it persists the object, and the CLI checks read-only-ness again before printing. The duplication is deliberate: the command field is model-authored, and the CLI reads RemediationAction objects straight out of the cluster, where an older operator or anything with permission to create the CRD could have put a kubectl patch in there.

What you see at the CLI

dorgu remediation diff names the workload and its owner in the header, and diffs workload against workload rather than persona against persona:
Two things to notice. The Owner: line is there on unmanaged workloads too, reading unmanaged (nothing reconciles it, so Dorgu may patch it), so you never have to infer which mode you are in. And the suggested action is not approve: printing a command that will be declined is how a guard that is working reads as a guard that is broken.

The refusal

If you run approve or heal on an owned workload anyway, you get the refusal rather than a patch:
Approval is withheld along with the patch. The gate sits in the preflight, ahead of any write, so on an owned workload approve writes nothing at all: no Deployment patch and no status patch. That is deliberate. Approving is what tells the operator to patch the persona and start the verification clock, so approving a change the CLI will not apply would leave the persona at 128Mi, the workload at 32Mi, and a ten-minute verification window running over a fix that was never coming.

Exit code 4: declined by design

A refusal exits 4 (ExitDeclined), not 1. The command ran, the plan was understood, and the decision was not to write. A script should be able to tell that apart from breakage.
Any wrapper that treats every non-zero exit as breakage will now report a successful refusal as an error. Update it to accept 4, or use --no-heal (which exits 0) where you want the decision recorded without a workload patch.

Recording the decision: --no-heal

--no-heal is how you say “I have read this, I agree, and I will apply it myself”:
It records phase: Approved on the RemediationAction, lets the operator patch the ApplicationPersona spec, and skips the Deployment patch entirely. Exit code 0.
The CLI warns that the persona and the running workload will disagree until you apply the change at the source. That warning is accurate and worth heeding: the operator’s verification window will run against a workload that has not changed yet.If you manage personas through GitOps, the persona patch will also show as drift on your next sync. Mirror it back into your repo so your pipeline does not revert it.

--workload no longer redirects the patch

--workload still resolves the Deployment, but it must agree with the one recorded in spec.workloadRef. A mismatch is refused:
Ownership is a fact about one specific object. A flag that aims the patch at a Deployment the operator never observed is the guard with a hole in it: Dorgu would clear frontend as unmanaged and then write to frontend-canary, which Helm owns.
--container still overrides freely, because ownership is per-Deployment, not per-container. When --container is omitted, the container the operator actually read is used, so the patch targets the same container whose values were the diff’s before-state.

What is not gated

Ownership governs one thing: the CLI patching your Deployment. Everything else is unchanged. Detection, diagnosis, proposal, guardrails, verification, and incident memory all run identically on owned and unmanaged workloads. The operator could not write your workloads if it wanted to: its ClusterRole grants no create, update, or patch on Deployments.

Version coupling

Current versions: CLI v0.12.0 with operator v0.11.0 (Helm chart 0.11.0; the chart version, the chart appVersion, and the operator version are always the same number). Newest of each is always the right answer, and this note is the only place in these docs that states which that is.There is no version pinning between the CLI and the operator. Every field either side has added is optional and additive, so a half that does not know about a field simply sees it absent and renders exactly as it did before. Nothing has to be upgraded in lockstep.Three mismatches change behaviour. Only the first is unsafe, and it is the old CLI, not the old operator:One operator upgrade is worth doing on its own merits, with no CLI implication at all: with aiRemediation.enabled=true, operator v0.11.0 is the release that made AI-planned remediations appliable. On v0.10.0 and older, a plan that diagnosed a resource change could be persisted with nothing to apply. See AI setup.Both mismatch directions in full, with the ownership reasoning behind them: version coupling. Newest published releases: CLI and operator.
The rest of this section is the history behind that note: which release introduced each half of the guard, and what each mismatch actually does. It states no “current” version of its own, so there is one place in these docs to correct when a release lands.

The ownership guard: CLI v0.10.0 with operator v0.9.0

A missing workloadRef is deliberately treated as owned, which makes both mismatch directions predictable rather than silent.
Of the two mismatches, CLI ≤ v0.9.0 against operator v0.9.0 is the dangerous one. The other direction is merely conservative: it declines things it could have patched. This one silently does the thing the guard exists to prevent.

The field-manager footprint: CLI v0.11.0 with operator v0.10.0

The two halves of the footprint fix are independent rather than coupled. Neither requires the other to run, and there is no dangerous mismatch between them, only a partial one.

Guardrail verdicts: CLI v0.12.0 with operator v0.11.0

Not coupled at all, and worth saying so explicitly, because it is the pair a reader is most likely to assume needs pinning. Operator v0.11.0 records what its guardrails decided on the optional spec.steps[].safety field; CLI v0.12.0 prints it. Either half runs alone. The reason this one exists at all is a reporting failure rather than a safety failure. The verdict used to be spliced onto the front of the model’s rationale as [safety:blast-radius] …, which in clean-room run #4 put Dorgu’s measurement one line below the model’s own claim that the same 16x change was “well within a 2x ceiling”, with nothing to tell the reader which of the two had been computed.

Trying it

The greenfield quickstart heals end to end, because its demo app is applied with kubectl apply and is therefore unmanaged. To see the ownership path instead, point Dorgu at anything you installed with Helm and break it through Helm:
Pick a limit the container genuinely cannot live within. If the pod comes back healthy, halve it and try again.
Break it through the owner, not with kubectl set resources. An earlier version of this walkthrough used kubectl -n apps set resources deploy/<deployment> --limits=memory=32Mi. It reaches the same broken state and Dorgu behaves identically: it detects, diagnoses, names the Helm release, and writes nothing at all. But kubectl set leaves a kubectl-set field manager owning resources.limits.memory, so the helm upgrade this walkthrough ends on fails with a field-manager conflict against kubectl-set and reads as though Dorgu broke the release. Dorgu never touched it, and the guard held. Setting the limit through helm upgrade leaves no conflicting manager, so the closing step tests the thing it is there to test.If you already ran the old version and are stuck on that conflict, clear the stale manager first:
Every manager, Helm included, reclaims its fields on its next apply.
The diff names the Helm release, shows the Deployment change, and offers --no-heal and reject instead of approve. Apply the fix where that chart’s values live, then run your usual upgrade and record the decision:
That helm upgrade succeeding afterwards is the whole point.

Next steps

Security and permissions

The operator’s actual ClusterRole, and the verbs it does not have

Self-healing

Detection, diagnosis, guardrails, and verification

dorgu remediation

Every flag for diff, approve, reject, and heal

Trust model

Where ownership sits in the progressive trust levels