skip to content

Policy Files as Source

Two hundred .rego files by three authors drift in style and stall on the v0-to-v1 keyword break. Interviewers ask how you migrated with no flag day and what stops the drift returning.

on this pageshow

questions

3

Why does a Rego rule written as deny[msg] { ... } fail to parse under OPA v1?

level: juniorimportance: must knowfreq 74%

answer

  1. two keywords stopped being optional
  2. the old head was ambiguous: set or object
  3. contains for sets, if before a body
  4. one import bridges a single file

basics

~20 s

OPA v1 makes if and contains mandatory, so a partial set rule must read deny contains msg if { ... }. The bare v0 head is a parse error, not a deprecation. import rego.v1 opts a single file into v1 syntax on an older engine.

solid answer

~50 s

In Rego v0 a rule head was followed directly by its body: `allow { ... }`, `deny[msg] { ... }`. Rego v1, which OPA 1.0 made the default, requires two keywords: `if` between a head and a body, and `contains` to declare a partial set rule. So the classic gate rule becomes `deny contains msg if { ... }` and a single-value rule becomes `allow if { ... }`. The old form does not warn — it fails to parse, so the module cannot be loaded at all. During a migration `import rego.v1` is the bridge: on a 0.x engine it opts that one file into v1 syntax and enforcement, so a repository converts file by file and each converted file is proven against the engine already running, well before the major upgrade lands. On a v1 engine the import grants nothing new.

code

rego · 18 lines
rego
# Rego v0 — parses on OPA 0.x, is a parse error under OPA v1
package volumes

deny[msg] {
    vol := input.spec.volumes[_]
    not vol.encrypted
    msg := sprintf("volume %v is not encrypted at rest", [vol.name])
}

# Rego v1 — same meaning, mandatory keywords
package volumes
import rego.v1

deny contains msg if {
    vol := input.spec.volumes[_]
    not vol.encrypted
    msg := sprintf("volume %v is not encrypted at rest", [vol.name])
}

go deeper

for a junior

Be ready to write the v1 form from memory: deny contains msg if { ... } and allow if { ... }. Know that the old form is a parse error, not a warning.

for a middle

Explain why contains exists at all — the v0 head could not be told apart from a partial object rule — and what import rego.v1 enforces on a single file during a migration.

for a senior

Show that you know how a non-compiling module actually surfaces where you run it: a failed startup, a rejected bundle with stale policy still serving, or nothing at all. Then say which CI check in the policy repo catches it first.

for a principal

Own the sequencing: convert files under import rego.v1 on the engine you already run, burn the count down with an advisory lint, make it blocking, and only then upgrade the engine so the major is a non-event.

## The break, precisely Rego v0 separated a rule head from its body with nothing but a brace: ``` allow { ... } deny[msg] { ... } p[k] = v { ... } ``` That was compact and ambiguous. `p[x] { ... }` declares a partial **set** rule, while `p[x] = y { ... }` declares a partial **object** rule; you had to read the whole head to know which one you were looking at, and so did every tool that processed the file. Rego v1 — the language version OPA 1.0 turned on by default — removes the ambiguity by promoting two keywords from optional to mandatory: - **`if`** must sit between a rule head and a rule body: `allow if { ... }`. - **`contains`** must declare a partial set rule: `deny contains msg if { ... }`. So the canonical policy-gate rule that collects violation messages is now: ``` deny contains msg if { ... } ``` The important part for anyone operating a gate is the failure mode. This is not a deprecation warning and not a behaviour change: the v0 head is a **parse error** under v1. A file that does not parse is not compiled, and a module that is not compiled is not part of the loaded policy set. ## What `import rego.v1` buys you Late in the 0.x line, individual new keywords could be opted into per file with `import future.keywords.if`, `import future.keywords.in`, `import future.keywords.contains` and `import future.keywords.every`. `import rego.v1` is the migration switch built on top of that: place it at the top of a file and that **one file** is parsed and enforced as v1 — all the new keywords are available, and the v1 requirements are applied, so the file fails on the old engine for exactly the reasons it would fail on the new one. The per-file granularity is the whole point. A repository holding hundreds of rule files does not have to convert in one commit synchronised with an engine upgrade. It converts a directory at a time, and every converted file is verified by the engine already in production. On a v1 engine the import is inert: it is accepted for compatibility and grants nothing that is not already the default. ## What does not change `import rego.v1` and the keyword rewrite do not change what a rule **means**. A set rule still collects messages; a rule whose body is undefined for a given input still produces no result rather than a false; a `default` still applies to a complete rule. If a rule stopped matching some input after a migration, the syntax rewrite is not the explanation — look at the input shape or the engine's other behaviour instead. Rules with no body need no `if`: `allow := true` and `deny contains "hard-coded finding"` are both fine as they stand. `if` is required before a **body**, not in every head. ## Why this is a source-quality problem The reason this shows up in interviews is not that the syntax is hard, it is that the consequence is quiet. Take an encryption-at-rest rule over managed volumes, written in v0 style and untouched for a year. The engine moves to a major that only speaks v1. What the operator sees depends entirely on how policy reaches the decision point: - If the process loads the tree at startup, it fails loudly and does not come up. - If policy is shipped as a bundle, the bundle is rejected on activation and the previously activated policy keeps serving — quieter, and it looks like nothing happened. - If the file was never handed to the compiler in the first place (a build glob that missed a subdirectory, a manifest that excluded the path), nothing errors anywhere and the gate simply stops asserting that requirement. In the last two cases the incident is an **enforcement gap**, not a parse error, and it is discovered by whoever is on call rather than by the author. That is why the v0-to-v1 move belongs in the policy repository's own CI rather than in an engineer's terminal: run `opa check` over the whole tree so a file that will not compile cannot merge, use `opa fmt` to do the mechanical rewriting, and let a Rego linter flag files still carrying the old syntax so the conversion has a visible burn-down. ## Migrating in practice A workable order: add `import rego.v1` to one file, fix what the parser complains about, run the existing gate against the same inputs to confirm the decisions are identical, then repeat. Keep the check that rejects unconverted files advisory until the burn-down is done, then make it blocking, and only then move the engine. The engine upgrade should be the boring step, because every file has already been proven against v1 rules on the old binary.

  • How does import future.keywords relate to import rego.v1?
    `import future.keywords.if` and its siblings make one new keyword available in a v0 file without requiring it. `import rego.v1` goes further: it turns on all of them and applies the v1 rules, so the file must use `if` and `contains` and fails the same way the new engine would fail it. That enforcement is what makes it a migration tool rather than a convenience. On a v1 engine the import is accepted for compatibility and grants nothing new.
  • Does every rule need if, even one with no body?
    No. `if` is required before a rule body, not in every head. `allow := true` and `deny contains "finding"` are complete rules with no body and stay exactly as they were. The break only bites rules that had a `{ ... }` body attached directly to the head, which in practice is nearly every real gate rule.
  • Can one policy repository hold v0 and v1 files at the same time?
    On a 0.x engine, yes — `import rego.v1` is per file, so converted and unconverted files load side by side and the conversion can burn down over weeks. Once the engine is a v1 major, every file must be v1; the compatibility mode for old syntax applies to the whole load, not to individual files, so it is an escape hatch for the estate rather than a per-file bridge.

It is a language release that turns a popular but ambiguous shorthand into a syntax error: the compiler stops guessing which kind of rule you meant and makes you say it.

saying these in an interview costs you the question

  • Claims import rego.v1 changes what a rule evaluates to
  • Thinks if is stylistic and the old form still parses
  • Says v1 removed partial set rules altogether
  • Expects the engine to auto-upgrade old syntax at load time
  • Treats a parse error as affecting only that one rule

context

open as a page

OPA allowed an unencrypted volume though a Rego rule forbids it — how do you check the rule was loaded?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Separate evaluated-and-found-nothing from never-loaded. Ask the running engine which modules it holds and whether its bundle activated, then replay the exact input against that same bundle. A bundle that fails to compile is rejected whole, leaving older policy serving.

open as a page

What does opa check --strict reject in a .rego file that plain opa check accepts?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Strict mode promotes lint-grade problems to compile errors: duplicate imports, unused imports, unused local assignments, using input or data as a variable or rule name, and deprecated built-in aliases. Plain opa check only reports genuine parse and compile errors.

open as a page