Two OPA bundles declare overlapping roots in their manifests — what happens?
answer
- roots are ownership, not documentation
- activation wipes the subtree first
- two owners of one path is forbidden
- missing roots claims the whole tree
- the rejection is quiet, not loud
basics
~20 sOPA requires bundle roots to be disjoint. It refuses to activate a bundle whose roots overlap another configured bundle's, records the error and keeps serving what it had, so the new rules silently never take effect.
solid answer
~50 sRoots are an ownership boundary, not a hint. A bundle's `roots` list the paths under the policy and data tree it owns, and on activation OPA erases everything under those roots before writing the bundle's content. Because two bundles cannot both own the same subtree, OPA enforces that roots across configured bundles are **disjoint** and refuses to activate a bundle that overlaps another, logging the conflict and leaving the previously activated state in place. The trap is the default: a `.manifest` with no `roots` (or no manifest at all) defaults to the empty root, meaning that bundle claims the entire document tree and therefore overlaps with every other bundle. The practical fix is to give each publishing team an explicit, non-overlapping prefix — one team owns `k8s/platform`, another `k8s/payments` — and to treat a missing `roots` in a multi-bundle setup as a configuration defect.
code
json · 4 lines{
"revision": "payments-2026-08-14-91c2",
"roots": ["k8s/payments"]
}go deeper
Know that a bundle's roots say which part of OPA's data and policy tree it owns, and that two bundles are not allowed to claim the same part.
Explain the mechanics: activation erases everything under the bundle's roots before writing, roots across bundles must be disjoint, and an omitted roots key defaults to the whole tree.
Recognise the symptom in production — a bundle that publishes cleanly but never activates — and know to inspect every configured bundle's manifest, not just the one that changed.
Design the root layout so each publishing team's blast radius is its own subtree, and set the rule that composition happens through data another bundle owns, never through a shared package.
## Roots as an ownership boundary OPA can be configured with more than one bundle, each polled from its own service and resource. That immediately raises the question of who owns which part of the document tree, and `roots` in the `.manifest` is the answer. A root is a slash-separated path prefix into the combined policy and data namespace, for example `k8s/payments`. Two rules follow from it: 1. **Everything the bundle ships must be inside one of its roots.** A Rego package or a data path outside the declared roots is a bundle error, not a silent import. 2. **Activation replaces the whole subtree.** OPA erases what is currently under the bundle's roots and writes the bundle's content in its place. This is what makes deletion work: drop a `.rego` file from the bundle, republish, and the rules it contained are gone from OPA after the next activation. It also means a bundle can never affect anything outside its roots, no matter what it contains. ## Why overlap is rejected If two bundles both claimed `k8s`, activation order would decide whose rules survive, and each poll of either bundle would wipe the other's content. Rather than allowing that race, OPA requires the roots of all configured bundles to be **disjoint** and refuses the activation when they are not, recording the conflict as a bundle error. The consequence is the part worth saying out loud: **the rejection is not loud to the team that caused it.** OPA keeps running on whatever it had already activated. The bundle service keeps serving happily. The team that just published a new rule sees a green pipeline and a published artifact, and their rule is simply not in the engine. You only see it in OPA's logs and in the status report's per-bundle error field. ## The default-root trap If a bundle has no `.manifest`, or a manifest with no `roots` key, the roots default to the empty root `""` — the whole document tree. That is fine and convenient in a single-bundle deployment, and it is exactly what most examples show. The moment a second bundle is added it becomes a conflict machine: the unscoped bundle claims everything, so it overlaps with any scoped bundle you add next. Teams hit this the first time they split a monolithic policy bundle into per-team bundles, and the fix is to add explicit roots to the original bundle in the same change that adds the second one. ## Designing the root layout A workable layout mirrors the ownership you actually have: | Bundle | roots | who publishes | |---|---|---| | platform baseline | `k8s/platform` | the platform team | | payments overlay | `k8s/payments` | the payments team | | shared reference data | `ref` | the data pipeline | Two things follow. First, a team can only ever break its own subtree, which is a meaningful blast-radius property when several groups publish independently. Second, cross-bundle references still work in one direction: policy in `k8s/payments` can read `data.ref`, because reading is not owning. Only writing is scoped. A common structural mistake is to try to have a team "extend" a baseline rule by shipping the same package from a second bundle. That is precisely the overlap OPA forbids. The composition has to happen inside a single owner's bundle — a baseline rule that reads a per-team data document another bundle owns — rather than by two bundles writing the same path. ## Diagnosing it Given "our new bundle publishes but the rule never fires", roots overlap belongs in the first three hypotheses along with a download failure and an activation error. All three look the same from outside: the active revision for that bundle is not the one you published, and the status report has an error string that names the cause. Checking the manifest of *every* configured bundle, not just the one you changed, is what makes overlap findable — the conflict is a property of the pair, and the bundle you did not touch is as likely to be the unscoped one.
- A bundle ships a Rego package that sits outside its declared roots. What happens?It is a bundle error and the bundle does not activate. Roots are not advisory scoping applied after the fact — every policy package and data path in the archive has to fall inside a declared root, otherwise OPA rejects the whole bundle and keeps the previously activated one. The usual cause is a package rename that was not mirrored in the manifest.
- How can a team-owned bundle still influence a rule owned by the platform bundle?By owning data the platform rule reads, not by shipping the same package. Reading across roots is unrestricted; only writing is scoped. So the platform bundle keeps the rule and consults, say, `data.payments.exemptions`, while the payments bundle owns and publishes that path from its own root. Two bundles writing the same package is exactly the overlap OPA refuses.
saying these in an interview costs you the question
- Thinks the second bundle simply wins and overwrites
- Treats roots as documentation rather than enforced scope
- Forgets that omitted roots claim the entire tree
- Expects a loud failure in the publishing pipeline
- Believes activation merges rather than replaces the subtree