skip to content

Your conftest rule requiring a PodDisruptionBudget for every Deployment never fires - why?

level: middleimportance: nice to knowfreq 30%

answer

  1. what one evaluation can see
  2. each document is its own input
  3. the rule spans a set, not an object
  4. combine gives path and contents
  5. silence here is not compliance

basics

~20 s

conftest evaluates each YAML document in the stream separately, so a rule can never see a Deployment and a PodDisruptionBudget at the same time. Combine the inputs into one evaluation, or the rule stays permanently undefined.

solid answer

~50 s

By default each document in a rendered multi-document stream becomes its own `input`, so a rule body that asserts something about a Deployment *and* something about a PodDisruptionBudget can never be satisfied: within the Deployment's evaluation the PDB does not exist, and the body is undefined - which produces no message rather than a denial. conftest's `--combine` flag changes the input shape to a single array whose elements each carry a file `path` and the parsed `contents`, so the rule can iterate over every document and join them. Note that this rewrites `input` for every rule in the run, so per-document rules must be adapted too. Even combined, the honest scope is this rendered stream: a PodDisruptionBudget defined in another chart or already present in the cluster is not in the input, so the rule can only assert what this artifact does or does not contain.

go deeper

for a junior

Know that a rendered chart is many documents and that the check looks at one document at a time, so a rule needing two different objects will not work as written.

for a middle

Explain the per-document input, what combine mode changes about the shape of input, and why an undefined rule body produces silence instead of a denial.

for a senior

Show you test that a rule can fire before trusting its green result, that you handle the label-selector join honestly, and that you state the rule's scope as a property of the artifact rather than of the cluster.

for a principal

Own the line between what a pre-deployment document gate can assert and what needs a runtime check, so the organisation does not bank assurance on a rule the input could never support.

## One document at a time A rendered chart is a multi-document YAML stream: Deployment, Service, ServiceAccount, PodDisruptionBudget, all separated by `---`. conftest parses that stream and, by default, evaluates the policy **once per document**. Inside each evaluation, `input` is that single object and nothing else. That makes a whole class of rule unwritable in the default mode. "Every Deployment must have a matching PodDisruptionBudget" is not a statement about a Deployment; it is a statement about a **set**. When the Deployment is the input, the PDB is not visible; when the PDB is the input, there is no Deployment to attach it to. The rule body is undefined in both passes, and in Rego undefined contributes no result - so the gate is green and has never once evaluated the control it was written for. This is the same silent-pass trap as an un-rendered chart, arriving by a different route. ## Combine mode `conftest test --combine <files>` changes the input shape: instead of N evaluations of one document each, there is **one** evaluation whose `input` is an array of elements, each carrying the source `path` and the parsed document as `contents`. Now the set is visible: ```rego package main deny contains msg if { some d in input d.contents.kind == "Deployment" labels := d.contents.spec.template.metadata.labels not pdb_for(labels) msg := sprintf("Deployment %s has no PodDisruptionBudget", [d.contents.metadata.name]) } pdb_for(labels) if { some p in input p.contents.kind == "PodDisruptionBudget" p.contents.spec.selector.matchLabels == labels } ``` Two things about that rule are worth saying out loud, because they are where a candidate shows judgment rather than syntax. **The join is a heuristic.** Kubernetes does not link a PodDisruptionBudget to a Deployment; the PDB selects *pods* by label, and the Deployment's pod template carries labels. Exact map equality, as written above, is stricter than the platform's own subset semantics: a PDB whose selector is a subset of the pod labels does select those pods, and this rule would reject it. Either implement the subset check or accept the stricter contract deliberately and document it. Silently mismatching here produces false positives, which cost you the room faster than false negatives cost you security. **Combine mode changes `input` for everything.** Rules written for the per-document shape (`input.kind == "Deployment"`) stop matching entirely once the input is an array - and, of course, they stop matching *silently*. Either keep set-scoped rules in their own run and package, or migrate every rule to the combined shape at once. A half-migrated policy directory is a directory of rules that quietly do nothing. ## The scope you can honestly claim Even in combine mode, the input is *this rendered stream*. A PodDisruptionBudget that lives in a different chart, is applied by the platform team, or already exists in the namespace is not in the documents you handed the engine, and the rule will flag a Deployment that is in fact protected. There are only three honest responses: 1. **Narrow the claim.** State the rule as "this chart must ship a PDB for its Deployments" - a real, checkable property of the artifact - rather than "this workload is protected", which is a property of the cluster. 2. **Widen the input.** Render everything that is applied together as one stream, so the set the rule reasons over matches the set that deploys. 3. **Move the check.** If the property is genuinely about cluster state, a pre-deployment document gate is the wrong place for it, and no amount of flag-twiddling makes it the right one. That is the point the parent topic keeps making: the shape of the artifact decides which rules are writable at all. A gate over a manifest stream can assert what the stream contains and how its documents relate to each other. It cannot assert anything about objects that are not in it, and a rule that appears to do so is either quietly undefined or quietly wrong. ## Diagnosing "my rule never fires" The general technique is worth carrying away. Because undefined is not false, a rule that never matches looks exactly like a rule that always passes. So when a rule has never produced a finding, do not assume compliance - prove the rule can fire. Feed it a document you know violates it and confirm you get the message. If a deliberately broken input still passes, the problem is the rule or the input shape, not the estate.

  • What breaks when you switch an existing policy directory to combine mode?
    Every rule written against the per-document shape, because `input` is now an array of `{path, contents}` elements rather than a single object. Those rules stop matching, and they stop matching silently - undefined yields no message. Migrate all rules together, or keep set-scoped rules in a separate run and package.
  • The rule now fires against a Deployment whose PodDisruptionBudget is applied by the platform team. Is the rule wrong?
    The rule is out of scope rather than wrong. The PDB is not in the rendered stream, so the input cannot support the claim being made. Either narrow the rule to "this chart ships a PDB for its Deployments", render everything that deploys together, or move the check to something that can see cluster state.
  • How do you prove a rule that has never produced a finding is actually working?
    Feed it an input you know violates it and confirm the expected message appears, and keep that case as a fixture. Because an unmatched body is undefined rather than false, a rule that cannot fire is indistinguishable from an estate that is fully compliant - and only a deliberate negative test tells them apart.

Asking a per-document rule about a missing PodDisruptionBudget is like asking someone reading one page at a time whether a chapter is missing from the book.

saying these in an interview costs you the question

  • Assumes a rule can see every document at once by default
  • Reads a rule that never fires as full compliance
  • Joins a PodDisruptionBudget to a Deployment by name and calls it exact
  • Switches to combine mode and leaves per-document rules unmigrated
  • Claims a workload is protected when the input is one chart's output

context