CEL Context Injection#
CEL context injection is the mechanism by which Kyverno resolves Kubernetes admission data — namespaceObject, object, oldObject, request, and variables — and makes them available as named variables inside CEL expression evaluation environments. This applies uniformly to ValidatingPolicy, MutatingPolicy, and DeletingPolicy policy types.
The canonical string keys for all context variables are defined in pkg/cel/compiler/keys.go:
| CEL Variable | Key Constant | Resolved From |
|---|---|---|
namespaceObject | NamespaceObjectKey | NamespaceResolver (or resource itself for Namespace kind) |
object | ObjectKey | admission.Attributes.GetObject() |
oldObject | OldObjectKey | admission.Attributes.GetOldObject() |
request | RequestKey | Raw admissionv1.AdmissionRequest (converted to map[string]any) |
variables | VariablesKey | Lazily-evaluated compiled variable programs |
exceptions | ExceptionsKey | Matched PolicyException objects |
These constants are shared across all policy type compilers, making the CEL activation map consistent regardless of policy kind.
Injection Pipeline Per Policy Type#
Each policy engine follows the same three-stage pattern: resolve → convert → inject.
ValidatingPolicy#
The engine resolves the namespace object from a NamespaceResolver if the request's namespace is non-empty, then builds a k8s.io/apiserver/pkg/admission.Attributes record . The Attributes record, the raw AdmissionRequest, and the namespace are passed to prepareK8sData, which:
- Calls
utils.ObjectToResolveVal(namespace)→ CEL-compatible value fornamespaceObject - Calls
utils.ObjectToResolveVal(attr.GetObject())→ value forobject - Calls
utils.ObjectToResolveVal(attr.GetOldObject())→ value foroldObject - Calls
utils.ConvertObjectToUnstructured(request)→map[string]anyforrequest
The resulting evaluationData struct is then spread into the CEL activation dataNew map in evaluateWithData.
userInfo is embedded in the request variable (as a field on the admission request map). Prior to PR #15619, vpol and mpol engines passed nil for the user context; the fix wires in admissionpolicy.NewUser(request.Request.UserInfo) when constructing admission.Attributes .
MutatingPolicy#
pkg/cel/policies/mpol/compiler/eval.go:prepareData mirrors prepareK8sData but accepts a typed *corev1.Namespace rather than runtime.Object. It also re-extracts object/oldObject from the raw AdmissionRequest and merges them back into the request map at the "object" and "oldObject" keys , ensuring the request body is self-consistent. The final activation map is returned directly .
namespaceObject support was added in PR #15625; before that fix the variable was absent from the MutatingPolicy CEL context.
DeletingPolicy#
DeletingPolicy's compiled Policy.Evaluate takes unstructured.Unstructured directly (no admission.Attributes). It calls utils.ObjectToResolveVal(namespace) and sets NamespaceObjectKey alongside ObjectKey and ResourceKey in the activation map . The engine resolves the namespace in Handle — namespaced resources use the NamespaceResolver, while cluster-scoped Namespace resources build a synthetic corev1.Namespace from the resource's own labels and then optionally override with the resolver's copy .
namespaceObject support was added in PR #14422.
CEL Environment Declaration#
Before any expression can reference a variable, it must be declared in the CEL environment at compile time. For ValidatingPolicy, this is done in createBaseVpolEnv, which registers all context variables . NamespaceType and RequestType are typed CEL objects (not dyn) so that field access is validated at compile time; object and oldObject use cel.DynType because they represent arbitrary Kubernetes resources.
DeletingPolicy and MutatingPolicy follow the same pattern in their respective compiler.go files. A missing variable declaration at compile time will cause expression compilation errors rather than runtime panics.
Nil Value Handling#
utils.ObjectToResolveVal (from github.com/kyverno/sdk/extensions/cel/utils) handles nil inputs — if the namespace resolver returns nil (e.g., for cluster-scoped resources or cache misses), the resulting CEL value is a null/null-like value rather than a panic. This is critical for namespaceObject, which is nil for cluster-scoped resources (except Namespace kind in DeletingPolicy, which has special synthetic-object logic).
For request, utils.ConvertObjectToUnstructured returns nil when passed a nil pointer; the mpol prepareData guards against this with an explicit if request != nil check before writing to the activation map.
For DELETE operations, attr.GetObject() returns nil and attr.GetOldObject() holds the deleted resource; ObjectToResolveVal(nil) correctly produces a null CEL value in those cases.
Bug Fix History#
Context injection was added incrementally per policy type. Several missing-variable bugs were fixed:
| PR | Policy Type | What was missing | Fix |
|---|---|---|---|
| #14422 | DeletingPolicy | namespaceObject not injected | Added namespace runtime.Object param to Evaluate; injected via utils.ObjectToResolveVal |
| #14599 | ValidatingPolicy | matchConstraints dropped at compile time | Stored matchConstraints in compiled Policy struct; engine uses CompiledPolicy.MatchConstraints() |
| #15619 | vpol + mpol | userInfo was nil (TODO placeholder) | Wired admissionpolicy.NewUser(request.Request.UserInfo) into admission.Attributes in all engine Handle methods |
| #15625 | MutatingPolicy | namespaceObject not injected | Added namespace field + injection in prepareData |
| #16200 | DeletingPolicy | namespaceSelector ignored for Namespace resources; namespaceObject always nil | Changed operation to admission.Create so matcher reads labels correctly; synthetic corev1.Namespace built from resource labels for cluster-scoped Namespace kind |
The pattern across fixes is consistent: the engine layer resolves the namespace object; the compiler/eval layer converts and injects it; missing variables cause silent nil or evaluation failures at runtime, not compile time.
Key Source Files#
| File | Purpose |
|---|---|
pkg/cel/compiler/keys.go | All context variable key constants (NamespaceObjectKey, ObjectKey, etc.) |
pkg/cel/policies/vpol/compiler/eval.go | prepareK8sData — converts admission attributes for ValidatingPolicy |
pkg/cel/policies/vpol/compiler/policy.go | evaluateWithData — builds CEL activation map and runs validations |
pkg/cel/policies/vpol/compiler/compiler.go | createBaseVpolEnv — declares CEL variables in the environment |
pkg/cel/policies/vpol/engine/engine.go | vpol engine: resolves namespace, builds admission.Attributes, calls Evaluate |
pkg/cel/policies/mpol/compiler/eval.go | prepareData — converts admission attributes for MutatingPolicy |
pkg/cel/policies/dpol/compiler/policy.go | DeletingPolicy Evaluate — builds activation map from unstructured.Unstructured |
pkg/cel/policies/dpol/engine/engine.go | dpol engine: resolves namespace (including Namespace kind special case), calls Evaluate |