skip to content

Your Rego deny rule names only the first container missing resource limits — how do you fix it?

level: seniorimportance: should knowfreq 36%

answer

  1. a comprehension is one value
  2. indexing it picks one element
  3. put the iteration back in the body
  4. the set collects one entry per binding
  5. three container lists, not one

basics

~20 s

The comprehension collapses every offender into one array, and indexing it at [0] picks a single name. Move the iteration into the rule body so each binding produces its own set entry, and iterate the container field names so init and ephemeral containers are covered too.

solid answer

~50 s

A comprehension evaluates to **one value** — an array of all the offenders — so `missing[0]` deliberately reports one of them, and the developer who re-renders the chart, fixes that container and resubmits gets refused again on the next one. Fix it by putting the iteration in the deny body itself: `some name in missing; msg := sprintf(...)` makes the partial set rule emit one message per binding, so all offenders arrive in a single decision. The second half of the fix is coverage: the comprehension walked only `containers`, while a pod spec also carries `initContainers` and `ephemeralContainers`. Bind the field name — `some field in ["containers", "initContainers", "ephemeralContainers"]; some c in input.spec.template.spec[field]` — and a workload with no init containers simply contributes no bindings instead of erroring. The offender list you build is also the input a mutating variant would use to inject defaults, so it is worth computing properly either way.

code

rego · 11 lines
rego
package helm.limits

missing := [c.name |
    some c in input.spec.template.spec.containers
    not c.resources.limits
]

deny contains msg if {
    count(missing) > 0
    msg := sprintf("container %v has no resource limits", [missing[0]])
}

go deeper

for a junior

Know that a comprehension collapses matches into one collection, so indexing it returns a single element. Reporting all offenders means iterating them rather than picking index zero.

for a middle

Explain the mechanics of the fix: an unbound variable in the deny body makes it evaluate once per binding, and a partial set rule collects one message per binding with no accumulator.

for a senior

Show the whole diagnosis — the one-name message, the missed init and ephemeral containers, and the reused-index trap — and explain what a partial refusal costs the team that has to re-render and resubmit.

for a principal

Own the standard: require rules to report the complete offender set, because the same set is what an actionable denial and any future auto-injection of defaults are both built from, and a one-at-a-time gate trains teams to route around it.

This is the complaint a platform team hears at five in the evening: a chart was rendered, the gate refused it, the message named one container, the developer fixed that container, re-rendered, and got refused again on a different one. Nothing is broken — the rule is doing exactly what it says — but the shape of the rule turns one review into four round trips. ## Why only one name comes out A comprehension is a single expression that produces a single value: ``` missing := [c.name | some c in input.spec.template.spec.containers; not c.resources.limits] ``` `missing` is an array. It is **always defined** — if nothing matches it is `[]`, never undefined, which is why `count(missing) > 0` is the right guard. But once the offenders are inside one array, any message built from `missing[0]` names one element by construction. The iteration that would produce several messages happened inside the comprehension and ended there; comprehension variables are scoped to the comprehension and do not leak into the body. ## The fix: bind in the body, let the set collect ``` deny contains msg if { some name in missing msg := sprintf("container %v has no resource limits", [name]) } ``` Now the body has an unbound variable again, so it is evaluated once per element, and the partial set rule collects one message per binding. No accumulator, no loop — the multiplicity comes from the bindings. (`contains` and `if` are the current keyword style; older policies wrote the same rule as `deny[msg] { ... }`.) If a complete rule had been used instead — `msg := ... if { some name in missing; ... }` — several distinct bindings would not silently pick one; evaluation fails with a conflict, because a complete rule may have only one value. That error is a useful signal that the rule shape is wrong for the job. ## The coverage half of the bug A pod spec has three container lists: `containers`, `initContainers` and `ephemeralContainers`. A rule that walks only the first will report a workload as clean while an init container runs unbounded. Rather than three near-identical rules, bind the field name and let iteration do the work: ``` missing contains c.name if { some field in ["containers", "initContainers", "ephemeralContainers"] some c in input.spec.template.spec[field] not c.resources.limits } ``` A variable can sit anywhere in a reference path, not just at an array index. If a spec has no `initContainers` key, that binding of `field` makes the reference undefined and simply contributes nothing — no error, no special case. Making `missing` a partial set rather than a comprehension also gives you deduplication for free, which matters when a chart renders several workloads with a shared sidecar name. ## The trap nobody spots in review The wrong way to cover two lists is to reuse one index variable: ``` some i c := input.spec.template.spec.containers[i] ic := input.spec.template.spec.initContainers[i] ``` Because `i` is unified across both references, only indices present in **both** arrays are considered. With three containers and one init container, exactly one pair is examined and everything else is silently skipped. The rule compiles, passes a superficial test with one container each, and under-reports in production. Use separate variables, or better, iterate values and never write an index at all. ## What this rule set is worth beyond the refusal The same `missing` set is the payload for two other things the platform team will be asked for. It is the body of an actionable denial — every offending container in one message rather than the first one. And it is the input to a mutating variant: the list of containers lacking limits, plus the defaults you would inject, is the same data whether you refuse the change or patch it on the way through. Computing the complete list is what makes either option available; a rule built around `missing[0]` can only ever refuse, one container at a time. ## Multi-document renders One last practical note: a rendered chart is a stream of documents, and how they reach the engine — one evaluation per document, or one array — changes the top of every path in the rule. Decide that before writing the paths, because a rule tested against a single manifest can quietly match nothing when handed a stream.

  • What happens if you keep the comprehension but build the message in a complete rule over several bindings?
    A complete rule may hold only one value, so distinct bindings produce a conflict error at evaluation rather than an arbitrary pick. That is a signal to switch to a partial set rule, which is built to collect one entry per satisfying binding.
  • Why not write three separate rules, one per container list?
    It works, but every future check has to be edited in three places and the third one gets forgotten. Binding the field name from a list keeps the assertion in one body, and a spec missing a list contributes no bindings rather than erroring — so absence needs no special case.
  • Someone covers both lists by reusing one index variable across containers and initContainers. What breaks?
    Unification forces the two references to share an index, so only positions present in both arrays are checked. Three containers and one init container means one pair examined and the rest silently skipped. It compiles and passes a naive test, which is what makes it dangerous — use separate variables or iterate values.
  • Is a comprehension ever the right choice here?
    Yes — when you want the offenders as data rather than as separate decisions: a count for a threshold, a list to embed in one summary message, or the input to a patch that injects defaults. The mistake is not the comprehension, it is indexing it and calling that a report.

saying these in an interview costs you the question

  • Thinks a comprehension yields multiple results like iteration
  • Adds a second rule instead of moving iteration into the body
  • Reuses one index variable across two container lists
  • Checks only spec.containers and calls the workload clean
  • Assumes a complete rule picks one value when bindings differ

context