Repository navigation
Ownership: answer what manages an object from its status lists and claims - #2319
Conversation
…lse winner Radar names an object's GitOps manager from two kinds of evidence that can disagree: managers' status lists, and the object's own tracking labels and annotations. View.Manager weighs both for an object and, when its own evidence doesn't settle it, for its nearest controller ancestor, and sums them up as shared, disagree, agree, listed, claimed, unknown or none. A disagreement stays one; none means no evidence in what Radar reads, not unmanaged. Claims are parsed in full, every candidate kept, a copied tracking ID marked. The answer reaches the wire as relationships.ownership.manager; existing fields are unchanged.
…anager answer A Deployment copies a Helm release's annotations onto its ReplicaSets, so on an object a controller owns that claim is inherited like an instance label: it no longer stops the walk short of a list naming the Deployment. A cluster-scoped object's tracking ID is compared without the namespace Argo CD writes into it. An access check Radar couldn't make keeps an answer from being none. The answer keeps the claims of the objects it walked past and says which controller reference it couldn't follow.
PR Summary by QodoResolve GitOps managers from status lists, claims, and controller ancestry
AI Description
Diagram
High-Level Assessment
Files changed (17)
|
Code Review by Qodo
1.
|
A controller of a kind discovery can't map left the answer's stop without the owner it names. The reference on the object the walk reached names it, as the primary owner's does.
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
Checking a stale claim read every object the claimed manager's list names, just to report that the list omits the claimant; a large Application made that thousands of reads per answer.
A tracking ID naming argocd/shop and an instance label naming shop made a claimed answer with no lead, as if they named two managers; the one that says its namespace now leads. An Application identity whose name holds an underscore, which Kubernetes names can't, makes no claim.
A controller that is gone, was replaced or is an Event ends the chain: no manager above it was missed, so an object nothing names reads none, not unknown.
… by list state A Pod whose ReplicaSet's instance label names an Application, under a Deployment with no evidence, answered none: the walk passed the only claim it found. The nearest ancestor claim now answers, as claimed via that ancestor, when the object's own metadata claims nothing. Argo CD's annotation+label tracking cuts the instance label to 63 characters; such a label now names the tracking ID's Application instead of a second one that doesn't exist. Checking a claim against a manager whose list omits the object read and sorted the whole list. ReadSession.Membership reads only the list's state.
…2316) ## Problem Radar adds an advisory warning when the target's metadata names another tool that may revert direct edits. This warning reaches MCP write tools, Diagnose and mutation verification through `pkg/k8score`'s object warnings. `detectExternalManager` looked up `argocd.argoproj.io/instance`, `kustomize.toolkit.fluxcd.io/{name,namespace}` and `helm.toolkit.fluxcd.io/{name,namespace}` in the object's **annotations**. Argo CD and Flux write these as **labels**, so the warning never fired for objects tracked by them; only Argo's `tracking-id` (a real annotation) and Helm were detected. The unit tests had placed the keys in annotations too, so they passed. ## Change Read those five keys from labels. The Argo `tracking-id` annotation and the Helm checks are unchanged. The test cases now carry the keys as labels, and a new case pins that the same keys as annotations are not tracking evidence. Once it fires, the warning has to say what the metadata says rather than state a verdict. Tracking metadata can be copied from another object or name a manager whose list no longer has the object, and native Helm acts only on an upgrade or rollback. So the sentence changes: - before: "Resource is reconciled by Argo CD (instance: shop). Direct edits may be reverted by the next reconciliation; …" - after: "Its tracking metadata names Argo CD (instance: shop). If that manager still tracks it, its next sync may revert direct edits; …" - Helm: "Its metadata says Helm installed it (instance: web). The next helm upgrade or rollback of that release may overwrite direct edits; prefer changing the chart values." Where a weighed manager answer is available (MCP `get_resource` and `diagnose`, #2320), that answer replaces the Argo CD, Flux and Helm warning, and #2320 also stops a tracking ID copied from another object from raising it (that check needs #2319's claim parser). ## Verification `go test ./k8score/` (pkg module) and `go test ./internal/mcp/` pass; with the old lookup, 7 of the detection cases fail. A wording test pins that neither sentence says the manager reconciles the object.
Which claims a state counts (never a copied one; native Helm only without other evidence), when a lead exists and that its namespace can be empty, the via fallback to a weak ancestor claim, between without via, when chainEnd is absent, what unchecked and missing mean, that a manager answer is absent rather than none when there is no answer, and the claim parser's real validation. gitops.HelmReleaseKind becomes unexported: nothing outside the parser reads it.
…he request The recorder search reused the view's earlier read of each manager it found: an earlier unavailable read silently dropped the manager (an answer could say none), and an earlier read of another incarnation confirmed membership from the replacement's list (agree, as Members already refused). Both now leave a gap, so the answer is unknown and the claim unchecked. A fallback to the nearest ancestor's claim kept only the claims before it; claims farther up the chain now stay in Between. Argo CD's truncated instance label also drops trailing non-alphanumerics; match that.
The manager answer's evidence carried unreadable, remote, notChecked and undecided. Nothing reads them; for the resource itself three repeat ownership.managers, and counts can't say which manager. They stay internal, where they still decide unknown from none; a reader that needs to explain an incomplete answer can add a field for it later without breaking anyone. An ancestor kept in Between only because its own search had gaps is no longer sent: without its gaps it would carry nothing. The drawer golden loses exactly the 97 removed keys.
Stacked on #2319 (the manager answer). ## Problem An AI agent asking "which GitOps manager should I change for this resource?" gets `resourceContext.managedBy`: the topmost owner the relationship graph links, or the first tracking label it finds. It never sees the managers' status lists, so it can't tell a confirmed answer from a stale label, and gets one answer where there are two. On a test cluster, a relabelled Flux Pod's `managedBy` named the Kustomization whose inventory lists it, and said nothing of the one its label claims. ## Change `get_resource` and `diagnose` (MCP) and the REST AI resource context now carry `resourceContext.manager`, compacted from #2319's answer: - `listedBy`: the managers whose status lists name the resource; - `claims`: the managers its metadata names, with each `check` (recorded, omitted, unreadable, remote, missing, not-recording, unresolved, withheld). Copied tracking IDs are dropped. - `state`, plus `lead` when the state has one; - `via`: the controller ancestor whose evidence answers when the resource's own doesn't, in which case `listedBy` and `claims` are that ancestor's; - `alsoClaimed`: claims on objects the walk passed (and, with `via`, on the resource itself), each `on` its object; - `narrowed`. With a manager answer in the context, `get_resource` and `diagnose` leave out the metadata warning for an Argo CD, Flux or Helm claim the answer already weighs ("Its tracking metadata names …", #2316); the warning beside it would contradict the answer when they disagree. Metadata the answer doesn't weigh keeps its warning: another controller's `app.kubernetes.io/managed-by` label (cert-manager, an operator), or Helm named by labels without its release annotation. A tracking ID copied from another object raises no warning on any path, and no longer hides an independent controller label behind it. Write tools and mutation verification, which carry no manager answer, keep every warning. `managedBy` is unchanged. The owner-only ownership answer resource contexts already used (`topology.ApplyOwner`) now also answers the manager. That costs one recorder-index lookup per object on the controller chain and a claimed manager's list read; there is still no children search. The test that guards this cost now allows exactly that and checks the answer equals the drawer's. `docs/mcp.md` gains a section, written for agents: `none` is not "unmanaged", and `disagree` means look at both. ## Verification - `pkg/resourcecontext` tests cover the compaction: via evidence, a copied claim dropped, narrowed, and walked claims kept without `via`. `pkg/k8score` tests cover copied IDs, an own Argo claim beside a cert-manager label, and Helm labels alone, on both the plain and the answered path; an `internal/mcp` test checks the answered path is used exactly when a manager answer is present. - `internal/mcp` and `internal/server` tests pass. - Live MCP `get_resource` on a test cluster: - the relabelled Flux Pod reads `disagree` via its Deployment, listed by `wg-ks-susp`, with its label's `wg-ks` claim `omitted`; - a Pod of a Deployment two Applications list reads `shared`, led by the Application its label claims; - an ordinary Argo CD Deployment reads `agree`.
Stacked on #2320, which is on #2319. ## Problem The header chip and the GitOps write guard decide "which manager owns this" in the browser: - the chip uses the first tracking label it finds, inherits a parent's manager one hop up, and guesses an Argo CD Application's namespace by name; - the write guard trusts whatever owner the browser sends and never checks the managers' status lists. With no owner it says nothing. So the chip and the warning could name a manager whose list doesn't include the object, or miss a second manager whose list does. ## Change **Chip.** When the server sends `relationships.ownership.manager` (#2319), the chip reads it: no browser inheritance, no guess by name. With an older Radar backend the chip behaves exactly as before; Radar Hub runs the newest UI against older Radars. | state | chip | tooltip | |---|---|---| | agree | Managed by X | the list naming it, and that its tracking metadata agrees | | listed | Listed by X | the list naming it; a list alone can name an object its manager doesn't sync (a copy carrying another object's tracking ID, a CRD a HelmRelease skips) | | via an ancestor | same wording, plus "via <Kind>" | "Through Deployment …", plus the object's own weak claims | | claimed | Tracked by X | why the claim couldn't be confirmed: omitted, unreadable, remote, missing, withheld, … | | disagree | Tracked by A, with a warning | A's list doesn't name it; B's does | | shared | Listed by A and B | no single manager's sync status shown | | unknown / none | no chip | — | With several candidates (disagree, shared, several claims) the chip opens a menu naming each and what names it (its status, its metadata or both); with one it opens that manager. A chip without a link is focusable, so keyboard users reach its tooltip. Related Resources' Managed By group is hidden only when the chip says exactly what it would. **Write guard.** - **The server decides.** `POST /api/gitops/write-evidence` computes the manager answer itself, as the caller, reading the live target's metadata, and returns it as `manager`. Its lead decides which manager's policy applies. The client's owner is only a hint when the server has no ownership answer for the kind. When the server computed one and couldn't finish (the object was replaced since the cache saw it, the cache isn't ready), `managerUnavailable` says why, and neither the hint nor the cached answer is used: the dialog warns and asks for an acknowledgement. - **Every cached answer is checked live,** including `none` and `unknown`: an object adopted since the cache saw it has a manager the cache doesn't. One request per guarded dialog. A failed request leaves a cached answer that names a manager unconfirmed (acknowledgement); a cached `none` stays quiet. - **The classifier composes.** The existing operation rules are unchanged: status writes say nothing, creating a child is informational, an operator-owned delete is informational. On top of them: - **disagree, shared, or several claims:** field writes get "may revert", naming every candidate, and no single candidate's ignore rules or sync policy exempt the change; - **an unconfirmed claim** (omitted, unreadable, remote, missing, withheld): can't prove "will revert" or waive the acknowledgement; - **"will revert" needs the list and the object's metadata to agree** (`agree`). A list alone (`listed`) gives "may revert", saying Radar can't confirm a sync applies the object, then the field evidence under "If one does:"; - **a CronJob's Job** (scheduled, or made with `kubectl create job --from`) runs once: deleting it doesn't say its controller recreates it; - **values from a newer Radar** (a state, check, source or chain end this UI doesn't know) render generically and read as unconfirmed, so they never prove a revert, waive a warning or link a manager; - **an adopted Helm install:** a native Helm release's annotations beside an Argo CD Application or Flux Kustomization keep "may revert" even when that manager ignores the field, since the release's next upgrade or rollback can still overwrite it; - **via an ancestor, with no claim of the object's own:** informational ("generated from <parent>, which X manages", or "which X lists", or "whose metadata names X", as the evidence allows), no acknowledgement; - **via, with the object's own claim:** keeps "may revert"; - **a chain Radar couldn't read** (owner withheld, unknown or unresolvable, a loop, too long): keeps the conservative "couldn't tell" warning. An owner that is gone or replaced ends the chain with an answer and adds no warning; - **no `manager` in the evidence** (older Radar): verdicts as before. ## Verification - **Tests.** Classifier tests cover each rule above, including: - an ignore rule on one of two managers still warning; - a status write on a shared object staying silent; - gone and replaced owners not warning. Chip tests cover every state's wording, plural names, and the group-hiding rule. Hook tests check the evidence query fires for ambiguous and inherited answers and that the server's answer wins over the client's. Server tests check `manager` in the response and that the server's lead overrides a conflicting client owner. A live-overlay test covers live metadata, a replaced UID, a cache switch and restricted access. - **Checks.** `make tsc` is clean. Frontend: 5,290+ tests across the UI package, 64 hook tests in the app. Go: `pkg/...`, `internal/k8s`, `internal/server`, `internal/mcp`. - **Live on a test cluster:** - a relabelled Flux Pod reads "Tracked by flux-system/wg-ks" with a warning; the tooltip says wg-ks's status doesn't list it, wg-ks-susp's does, through its Deployment; - a Pod of a Deployment two Applications list reads "Listed by …-a and …-b"; - an ordinary Argo CD Deployment reads "Managed by … · Synced", and its Managed By group is hidden; - the disagreeing Deployment's Set image dialog warns that both Kustomizations may overwrite the fields and requires the acknowledgement; - the write-evidence endpoint answers a Pod through its Deployment's Application even when the client sends a different owner.
This is #2325's change, reopened against `main`. #2325 merged into its stacked base branch after #2320 had already merged, so none of it reached `main`. Same code as #2325's final head, with current `main` merged in, plus shorter docs: `docs/ownership.md` holds the manager-answer rules (states as a table), the `docs/mcp.md` section keeps what an agent needs, and the `docs/gitops.md` write-warning rules are one table of answer by operation. ## Problem The header chip and the GitOps write guard decide "which manager owns this" in the browser: - the chip uses the first tracking label it finds, inherits a parent's manager one hop up, and guesses an Argo CD Application's namespace by name; - the write guard trusts whatever owner the browser sends and never checks the managers' status lists. With no owner it says nothing. So the chip and the warning could name a manager whose list doesn't include the object, or miss a second manager whose list does. ## Change **Chip.** When the server sends `relationships.ownership.manager` (#2319), the chip reads it: no browser inheritance, no guess by name. With an older Radar backend the chip behaves exactly as before; Radar Hub runs the newest UI against older Radars. | state | chip | tooltip | |---|---|---| | agree | Managed by X | the list naming it, and that its tracking metadata agrees | | listed | Listed by X | the list naming it; a list alone can name an object its manager doesn't sync (a copy carrying another object's tracking ID, a CRD a HelmRelease skips) | | via an ancestor | same wording, plus "via <Kind>" | "Through Deployment …", plus the object's own weak claims | | claimed | Tracked by X | why the claim couldn't be confirmed: omitted, unreadable, remote, missing, withheld, … | | disagree | Tracked by A, with a warning | A's list doesn't name it; B's does | | shared | Listed by A and B | no single manager's sync status shown | | unknown / none | no chip | — | With several candidates (disagree, shared, several claims) the chip opens a menu naming each and what names it (its status, its metadata or both); with one it opens that manager. A chip without a link is focusable, so keyboard users reach its tooltip. Related Resources' Managed By group is hidden only when the chip says exactly what it would. **Write guard.** - **The server decides.** `POST /api/gitops/write-evidence` computes the manager answer itself, as the caller, reading the live target's metadata, and returns it as `manager`. Its lead decides which manager's policy applies. The client's owner is only a hint when the server has no ownership answer for the kind. When the server computed one and couldn't finish (the object was replaced since the cache saw it, the cache isn't ready), `managerUnavailable` says why, and neither the hint nor the cached answer is used: the dialog warns and asks for an acknowledgement. - **Every cached answer is checked live,** including `none` and `unknown`: an object adopted since the cache saw it has a manager the cache doesn't. One request per guarded dialog. A failed request leaves a cached answer that names a manager unconfirmed (acknowledgement); a cached `none` stays quiet. - **The classifier composes.** The existing operation rules are unchanged: status writes say nothing, creating a child is informational, an operator-owned delete is informational. On top of them: - **disagree, shared, or several claims:** field writes get "may revert", naming every candidate, and no single candidate's ignore rules or sync policy exempt the change; - **an unconfirmed claim** (omitted, unreadable, remote, missing, withheld): can't prove "will revert" or waive the acknowledgement; - **"will revert" needs the list and the object's metadata to agree** (`agree`). A list alone (`listed`) gives "may revert", saying Radar can't confirm a sync applies the object, then the field evidence under "If one does:"; - **a CronJob's Job** (scheduled, or made with `kubectl create job --from`) runs once: deleting it doesn't say its controller recreates it; - **values from a newer Radar** (a state, check, source or chain end this UI doesn't know) render generically and read as unconfirmed, so they never prove a revert, waive a warning or link a manager; - **an adopted Helm install:** a native Helm release's annotations beside an Argo CD Application or Flux Kustomization keep "may revert" even when that manager ignores the field, since the release's next upgrade or rollback can still overwrite it; - **via an ancestor, with no claim of the object's own:** informational ("generated from <parent>, which X manages", or "which X lists", or "whose metadata names X", as the evidence allows), no acknowledgement; - **via, with the object's own claim:** keeps "may revert"; - **a chain Radar couldn't read** (owner withheld, unknown or unresolvable, a loop, too long): keeps the conservative "couldn't tell" warning. An owner that is gone or replaced ends the chain with an answer and adds no warning; - **no `manager` in the evidence** (older Radar): verdicts as before. ## Verification - **Tests.** Classifier tests cover each rule above, including: - an ignore rule on one of two managers still warning; - a status write on a shared object staying silent; - gone and replaced owners not warning. Chip tests cover every state's wording, plural names, and the group-hiding rule. Hook tests check the evidence query fires for ambiguous and inherited answers and that the server's answer wins over the client's. Server tests check `manager` in the response and that the server's lead overrides a conflicting client owner. A live-overlay test covers live metadata, a replaced UID, a cache switch and restricted access. - **Checks.** `make tsc` is clean. Frontend: 5,290+ tests across the UI package, 64 hook tests in the app. Go: `pkg/...`, `internal/k8s`, `internal/server`, `internal/mcp`. - **Live on a test cluster:** - a relabelled Flux Pod reads "Tracked by flux-system/wg-ks" with a warning; the tooltip says wg-ks's status doesn't list it, wg-ks-susp's does, through its Deployment; - a Pod of a Deployment two Applications list reads "Listed by …-a and …-b"; - an ordinary Argo CD Deployment reads "Managed by … · Synced", and its Managed By group is hidden; - the disagreeing Deployment's Set image dialog warns that both Kustomizations may overwrite the fields and requires the acknowledgement; - the write-evidence endpoint answers a Pod through its Deployment's Application even when the client sends a different owner.
Problem
Radar names an object's GitOps manager in several places that disagree. Two of them sit in the same drawer:
relationships.managedBy: the graph's topmost owner, else the first tracking label it finds, plus a one-hop inheritance the browser does itself;relationships.managers: the Argo CD Applications and Flux objects whose status lists name the object itself, never its controller.They use different evidence and disagree in ordinary cases. A Pod carries its Application's instance label but only its Deployment is listed, so the chip names a manager and the group shows none (73 objects on one test cluster). A relabelled object can claim one manager while another's list names it. Neither the MCP resource context nor the GitOps write guard sees status lists at all.
Change
This PR adds one answer, computed server-side from the same per-request ownership view the drawer already uses. It changes no existing output: nothing reads the new answer yet. The header chip and the write guard move to it in a follow-up PR, together.
ownership.View.Manager(ref)gathers two kinds of evidence:It sums them up as one state, first match wins:
shareddisagreeagreelistedclaimedunknownnoneIt names no winner the evidence doesn't support: a disagreement stays one, with every candidate kept.
Controller chain. When the object's own evidence doesn't settle the question, the answer walks Pod → ReplicaSet → Deployment and reports the nearest ancestor that does as
via. Some claims are weak and don't stop the walk:When nothing on the chain settles it and the object makes no claim of its own, the nearest ancestor that claims a manager answers as
via, withclaimed; claims farther up stay inbetween. A walk that can't follow an owner says which reference and why (stop). A manager this request read differently earlier (still loading, or another incarnation) leaves the answer incomplete rather than confirming membership.Claim parsing is new (
pkg/gitops/claims.go). It keeps every candidate rather than the first hit, and parses Argo CD's tracking ID in full: thens_appform, the core group, and cluster-scoped objects, whose namespace Argo CD writes but ignores. With annotation+label tracking, an instance label Argo CD cut to 63 characters names the tracking ID's Application. A native Helm release's claim counts only without GitOps evidence, since Flux's helm-controller writes the same annotations.Wire (additive).
relationships.ownership.managerholds{state, lead?, direct, via?, between?, chainEnd?, stop?, narrowed?}, with matching TypeScript types. Each evidence object carriesobject,recordersandclaims. Which manager lists Radar couldn't read or search stays internal: the state says whether that left the answerunknown, andownership.managersstill says what the object's own search missed. Readers should show an unfamiliar state, check, source or chain end generically: newer Radars may add values.relationships.managedByandrelationships.managersare unchanged.managedBy's topmost-owner meaning is still read by workload selection and summary grouping.View.RecordingManagersnow searches each object once per view, so the existing projection and the new answer share one search.Verification
managerfields; every existing answer is byte-identical.none1,655,claimed242,agree44,shared9,listed6,disagree5,unknown4.disagreeis exactly the relabelled Flux fixture;sharedis the two-Application fixture and a second tracking ID.unknownis the broken-chain fixtures Radar can't follow (an unserved owner kind, a loop). An owner that is gone or was replaced ends the chain with an answer.none642,claimed236,agree63. Mostclaimedanswers name an Argo CD Application by an instance label without a namespace, for an Application that isn't in this cluster.withheldorunresolved, neveromitted, and every answer isnarrowed.Not in this PR