How do you share Rego helpers across Gatekeeper ConstraintTemplates?
answer
- a template is one self-contained object
- helpers ship inside spec.targets
- the package must sit under lib
- your build splices the shared source in
- helpers take the object as an argument
basics
~20 sThrough the ConstraintTemplate's libs field: each helper is Rego source shipped inside the same object, under the lib package namespace and imported as data.lib.<name>. A template is self-contained, so every one carries its own copy.
solid answer
~50 sA ConstraintTemplate carries an optional `libs` list next to its `rego` block. Each entry is a Rego module that must declare a package under `lib`, and the policy imports it as `data.lib.<name>`. The important consequence is that a template is a single self-contained Kubernetes object: there is no module path, no registry, no filesystem, and no way to reference another template's library. So the helper lives once in your repository and a render step splices it into every template before they are applied — templates become build output, not hand-edited manifests. And you cannot simply reuse the helper library your pipeline checks already use, because the shapes differ: a manifest check sees the document as `input`, while a Gatekeeper rule sees the object at `input.review.object`. Helpers that take the object as an argument port across; helpers that read `input` directly do not.
code
yaml · 27 linesapiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredprobes
spec:
crd:
spec:
names:
kind: K8sRequiredProbes
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredprobes
import data.lib.pods
violation[{"msg": msg}] {
c := pods.containers(input.review.object)[_]
not c.readinessProbe
msg := sprintf("container %v has no readinessProbe", [c.name])
}
libs:
- |
package lib.pods
containers(obj) = cs {
cs := obj.spec.containers
}go deeper
Know that helpers ship inside the ConstraintTemplate itself, in its libs list, under a package in the lib namespace, and are imported as data.lib.<name>.
Explain why there is no import from outside the object: the template is a single stored resource with no module resolution, so every template carries its own copy of the helper.
Show how you prevent copies drifting — templates rendered from one source, applied as a set, and each rendered template tested against a known-bad fixture before rollout.
Own the library as a product: who may add a helper, how a breaking change is rolled out across every template and team, and what the contract is between shared helpers and their callers.
## The mechanism Inside a ConstraintTemplate, `spec.targets[0]` holds the `rego` block. Alongside it, `libs` is a list of additional Rego modules. Gatekeeper requires each of those modules to declare a package under the `lib` namespace — `package lib.pods` — and the main policy pulls it in with `import data.lib.pods`, then calls into it normally. That is the entire sharing mechanism, and its shape matters more than its syntax. ## A template is one self-contained object A ConstraintTemplate is a Kubernetes resource. When the API server stores it, it stores exactly what you sent: policy text and library text, inline. There is nothing else in scope. Concretely: - there is **no file path or import path** to somewhere outside the object; - there is **no registry or dependency resolution** step that fetches a module version; - **one template cannot import another template's library**, even if both are applied to the same cluster; - there is **no vendoring directory**; the library either lives inside this object or it does not exist for this policy. So "sharing" is not sharing at runtime. Every template that uses the helper carries its own copy of the source. ## Which makes templates build output The practical pattern that falls out is: the helper lives once, in a repository, as an ordinary Rego file with its own unit tests. A render step reads it and injects it into the `libs` of every template that declares a dependency on it, producing the manifests you apply. Then: - changing the helper means **re-rendering and re-applying every template that uses it**, and your pipeline should do all of them together rather than leaving a partially-updated fleet; - nobody hand-edits an applied ConstraintTemplate, because the next render would silently overwrite it; - the thing you test before rollout is the **rendered template**, not just the helper in isolation — a helper that passes its own unit tests can still be spliced into a policy that does not do what you meant. Without that discipline, copies drift: two templates end up carrying two versions of the same helper, and the bug you fixed once is still live in half the fleet. That drift is the real risk this feature introduces, and it is what an interviewer is probing for. ## Why the pipeline's rule library does not just port over The tempting question is: we already have a Rego library for the checks our pipeline runs over manifests — can the template import that? Two separate reasons it is not that simple. The first is mechanical, and covered above: there is nowhere for the template to import *from*. Reuse means copying the source in at build time, not referencing it. The second is semantic, and it bites even after you copy the file in. The two callers do not agree on where the object is. A check that runs over a manifest treats the document as `input` and reaches straight into `input.spec.containers`. A Gatekeeper rule sees the object at `input.review.object`, with the Constraint's configuration alongside at `input.parameters`. A helper written against the first shape returns nothing useful under the second — and because a helper that finds nothing simply contributes nothing, the resulting policy is inert rather than broken, which is the worst way for it to fail. The fix is a discipline, not a tool: **shared helpers take the object as a parameter and never read `input` themselves.** `containers(obj)` ports between callers; `containers` that reads `input.spec.containers` is welded to one of them. Each caller then does its own one-line extraction — the manifest check passes `input`, the template passes `input.review.object` — and everything below that line is genuinely shared. ## What good looks like A mature Gatekeeper rule repository usually has: helper modules under `lib`, each with unit tests; templates as templates in the build-system sense, with the helper spliced in; a check in CI that every rendered template still blocks its known-bad fixture and admits its known-good one; and a single apply step for the whole set, so a helper change lands everywhere at once or nowhere.
- How do you stop one helper change from drifting across forty templates?Treat templates as build output. The helper lives once in the repository, a render step injects it into every template that declares it, and CI re-renders and applies the whole set together. Nobody edits an applied template by hand. If a change cannot be rolled out to all of them at once, that is a release-sequencing decision, not something to leave to whoever notices.
- Why must a shared helper take the object as an argument rather than reading input?Because the callers disagree about where the object is: a manifest check sees the document as `input`, a Gatekeeper rule sees it at `input.review.object`. A helper that reads `input` directly is bound to one caller, and under the other it quietly finds nothing — which makes the policy inert rather than failing loudly. Passing the object in keeps the helper portable.
saying these in an interview costs you the question
- Expects one template to import another template's package
- Thinks libs can pull a module from a path or registry
- Puts a helper in a package outside lib and expects it to resolve
- Copies helpers by hand into each template
- Assumes a pipeline helper reading input works unchanged in a template