Kubernetes · GitOps

Two Tools,
the Same Missing Root

Argo CD and Flux express an application dependency tree in almost opposite ways — one signal lives entirely on the parent, the other is declared by each child. Audit either one without including the object that actually carries the proof, and the same false failure shows up in both, for what turns out to be the same underlying reason.

A check meant to confirm a set of applications forms a legitimate dependency tree — not a pile of unrelated ones that happen to share a naming pattern — has to answer a specific question: is this child genuinely managed as part of a larger structure, or is it standing alone? Two different GitOps tools answer that question with two structurally different signals, and both of them turned out to have the same failure mode when audited incompletely.

Argo CD: the Signal Lives on the Parent

Classic Argo CD App-of-Apps has a root Application with source.directory.recurse: true, pointed at a directory of child Application manifests. Confirmed against a real, reconciling Argo CD v3.4.5 controller, not just documentation: the children carry no distinguishing content at all. No ownerReferences, nothing in their own spec that says "I belong to a tree." The entire proof that they're managed, rather than standalone, lives on the root's recurse flag — an object the children themselves say nothing about.

Which means a check fed only the children — say, an audit scoped to "the apps my team owns," deliberately excluding a shared root someone else manages — has no way to tell two legitimately-managed children apart from two unrelated one-off Applications that happen to look similar. It reports FAIL either way, correctly reflecting that it can't confirm what it wasn't given the evidence to confirm.

Flux: the Signal Is Declared by the Child

Flux does the opposite. A child Kustomization declares its own membership explicitly: spec.dependsOn: [{name: infra}], naming the parent it depends on. On paper this looks like it should be self-sufficient — the child is already saying who its parent is, no external root object required to interpret it.

It isn't, for a specific reason: a name in dependsOn is just a string. Confirmed against a real flux install, not just the CRD schema: the kustomize-controller never stamps an ownerReferences back onto the child pointing at the parent, so nothing in the child's own live state proves that infra refers to something real and legitimately managed rather than a typo or a stale reference to a Kustomization that was deleted months ago. A check handed only the children — payment-api and order-api, each declaring dependsOn: [{name: infra}] — sees the same string both times and has no way to confirm it resolves to anything. It reports FAIL, on purpose, because a name a child claims and a name that's actually backed by a real, present resource are two different facts, and only one of them was in evidence.

Same failure, opposite mechanism

Argo CD's proof lives on the parent and says nothing on the child. Flux's proof lives on the child and says nothing back from the parent. They fail for structurally different reasons — one because the child is silent, the other because the child's claim is unverifiable alone — but the practical consequence is identical: leave the root out of what you feed the checker, and legitimate structure looks indistinguishable from a coincidence.

The Same Caveat, Confirmed Twice Independently

The Argo CD finding came first, against a real cluster running a self-contained App-of-Apps example. The natural next question — does the same root-exclusion problem apply to Flux's dependsOn tree — got answered the same way: a real flux install, a root infra Kustomization, two dependents declaring dependsOn on it, and a fixture built specifically to feed the checker only the children. It failed exactly as predicted, for the reason predicted. Two different tools, two structurally different ways of expressing a tree, and the same root-exclusion caveat held in both — not because the mechanisms were similar, but because "prove this object belongs to a tree" always needs at least one object outside the one being questioned.

The General Rule

Any check that verifies membership in a hierarchy — not just GitOps app trees, but ownership graphs, dependency graphs, org-chart-shaped permission audits, anything where "is this legitimately part of a structure" is the question — has to include the structure's root in its input, not just the leaf under review. A query scoped to "only what I own" or "only the thing I'm checking" can look complete and still be structurally unable to answer the question it was asked, because the proof it needs was never in scope to begin with.

Further reading

kyhsa93/k8s-playbook — both live-controller validation rounds, Argo CD and Flux, and the fixtures that reproduce each failure on purpose