Running a Compiled ValidatingAdmissionPolicy
Use runtime::Builder exactly as you would for any other compiled CEL module.
Pass the variable bindings as a JSON object:
#![allow(unused)]
fn main() {
use ferricel_core::{compiler::Builder, runtime};
let wasm_bytes = Builder::new().build().compile_vap(&yaml)?;
let result_str = runtime::Builder::new()
.with_wasm(wasm_bytes)
.build()?
.eval(Some(&bindings_json))?;
let result: serde_json::Value = serde_json::from_str(&result_str)?;
// result["accepted"] == true / false
}
build() (and build_pre()) check the module’s ABI version before they
link it. A module compiled by a ferricel with an incompatible ABI makes
build() return Err, naming both ABI versions. See
ABI Version in the Wasm Spec chapter.
Runtime Errors and failurePolicy
eval() returns Err when a matchConditions or validations expression
fails at runtime (for example, division by zero, an unbound variable, or a
kw.k8s extension that returns an error). The error downcasts to
ferricel_core::CelRuntimeError. Its Display text starts with
CEL runtime error:. The module never turns such a failure into an accept or
a reject response.
eval() can also fail for reasons that are not CEL runtime errors: an
epoch-deadline interrupt, a memory limit, a Wasm trap, or a bug in a host
extension. These errors do not downcast to CelRuntimeError.
The host is responsible for applying the policy’s failurePolicy. With
downcast_ref, the host can tell a CEL runtime error apart from other
failures and decide how each kind maps to failurePolicy:
#![allow(unused)]
fn main() {
use ferricel_core::CelRuntimeError;
match engine.eval(Some(&bindings_json)) {
Ok(result_str) => {
let result: serde_json::Value = serde_json::from_str(&result_str)?;
// result["accepted"] == true / false
}
Err(err) => match err.downcast_ref::<CelRuntimeError>() {
Some(cel_err) => {
// The CEL expression evaluated to an error. Apply failurePolicy:
// Fail -> deny the request, report `cel_err.message`
// Ignore -> allow the request
}
None => {
// Not a CEL runtime error: a deadline, a resource limit, a trap,
// or a host bug. The host decides how to handle it.
}
},
}
}
CelRuntimeError has two fields:
| Field | Type | Description |
|---|---|---|
message | String | The error message, for example divide by zero. |
origin | Option<ExtensionOrigin> | The host extension that produced the error, or None. |
ExtensionOrigin holds the namespace (for example Some("kw.k8s")) and
the function (for example get) of the extension call. When the params
lookup fails, origin is kw.k8s.get. A host can use this to tell a failed
params lookup apart from other runtime errors.
See Runtime Errors for the exact rules.
Required Bindings
| Binding | Required when… |
|---|---|
object | Always (the resource being admitted) |
oldObject | Policy expressions reference oldObject |
request | Policy expressions reference request |
namespaceObject | Policy expressions reference namespaceObject |
paramRef | paramKind is set (see below) |
object, oldObject, and request correspond directly to the fields of the
Kubernetes
AdmissionReview
request object.
Determining whether a given policy actually references oldObject,
request, or namespaceObject — without evaluating the policy or re-parsing
its CEL/YAML source — is exactly what the ferricel.vap-variables custom
section is for. Read it with ferricel_core::vap_variables_used() (or
ferricel inspect --json) at policy-setup time and only fetch/bind what is
actually needed. This is particularly relevant for namespaceObject, since
fetching it requires an extra host-side lookup — see
Custom sections and inspection
for details.
Kubernetes Resource Fetching
Params
When a policy sets paramKind, the compiled module fetches the referenced
resource itself at evaluation time by calling a host-provided kw.k8s.get
extension. The host does not supply params directly in the bindings.
The module reads paramRef.name and paramRef.namespace from the bindings at
runtime and forwards them to the host as part of the request map (see below).
The result is stored in params and made available to all variables and
validations expressions.
The host must supply paramRef in the bindings:
{
"paramRef": { "name": "my-params", "namespace": "default" },
"object": { ... }
}
And register a kw.k8s.get implementation on the runtime builder:
#![allow(unused)]
fn main() {
use ferricel_core::{compiler::Builder, runtime, compiler::vap};
let wasm_bytes = Builder::new().build().compile_vap(&yaml)?;
let result_str = runtime::Builder::new()
.with_wasm(wasm_bytes)
.with_extension(vap::kw_k8s_get_extension(), |args| {
// args[0] is the accumulated request map (see shape below)
let map = &args[0];
let name = map["name"].as_str().unwrap();
let namespace = map["namespace"].as_str().unwrap();
let api_version = map["apiVersion"].as_str().unwrap();
let kind = map["kind"].as_str().unwrap();
// Fetch from Kubernetes and return the resource as a JSON value.
let resource = fetch_from_k8s(api_version, kind, namespace, name)?;
Ok(resource)
})
.build()?
.eval(Some(&bindings_json))?;
}
Fetching Data from the Kubernetes API
The
kw.k8sAPI is implemented as a builder chain. See the Host Extensions chapter for general documentation on declaring and consuming builder chains.
Policy variables (and other expressions) can call kw.k8s directly to fetch
arbitrary resources. The API mirrors the
kw.k8s Kubernetes library
provided by the Kubewarden CEL policy:
kw.k8s
.apiVersion(<string>) → kw.k8s.ClientBuilder
.kind(<string>) → kw.k8s.Client
.namespace(<string>) → kw.k8s.Client (optional)
.labelSelector(<string>) → kw.k8s.Client (optional)
.fieldSelector(<string>) → kw.k8s.Client (optional)
.fieldMask(<string>) → kw.k8s.Client (optional, repeatable)
.get(<string>) → dyn (host call — returns one resource)
.list() → dyn (host call — returns a list)
Example — fetch a ConfigMap in a variable, then check a field in a validation:
// variables entry
kw.k8s.apiVersion('v1').kind('ConfigMap').namespace('default').get('my-config')
// validation expression
variables.cfg.data.allowedTeam == request.userInfo.groups[0]
Host Extension Request Map
When a kw.k8s.get or kw.k8s.list terminal is called, the host receives a
single argument — a JSON object containing the accumulated builder state:
| Key | Set by chain step | Notes |
|---|---|---|
apiVersion | .apiVersion() | Always present |
kind | .kind() | Always present |
namespace | .namespace() | Present only if .namespace() was called |
labelSelector | .labelSelector() | Present only if called |
fieldSelector | .fieldSelector() | Present only if called |
fieldMasks | .fieldMask() | Array; present only if called |
name | .get(<name>) | Present only for get terminal |
Register the extensions using the helpers from ferricel_core::compiler::vap:
#![allow(unused)]
fn main() {
use ferricel_core::compiler::vap;
// For policies that call .get(...)
runtime::Builder::new()
.with_extension(vap::kw_k8s_get_extension(), |args| { ... })
// For policies that call .list()
runtime::Builder::new()
.with_extension(vap::kw_k8s_list_extension(), |args| { ... })
}
Example
This example mirrors the scenario from the
Kubewarden CEL policy README:
a policy that enforces a maximum replica count read from a ConfigMap parameter resource.
The ValidatingAdmissionPolicyBinding
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "demo-policy-binding.example.com"
spec:
policyName: "demo-policy.example.com"
validationActions: [Deny]
paramRef:
name: "my-params"
namespace: "default"
parameterNotFoundAction: Deny
matchResources:
namespaceSelector:
matchLabels:
environment: test
The ConfigMap Parameter Resource
apiVersion: v1
kind: ConfigMap
metadata:
name: my-params
namespace: default
data:
maxreplicas: "5"
The Incoming Deployment
This is the resource being admitted:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:latest
Rust Integration
#![allow(unused)]
fn main() {
use ferricel_core::{compiler::{Builder, vap}, runtime};
// The host extracts these from the AdmissionReview and the PolicyBinding.
let bindings = serde_json::json!({
"paramRef": { "name": "my-params", "namespace": "default" },
"object": object_json,
});
let wasm_bytes = Builder::new().build().compile_vap(vap_yaml)?;
let result_str = runtime::Builder::new()
.with_wasm(wasm_bytes)
.with_extension(vap::kw_k8s_get_extension(), |args| {
// The module calls this to fetch the ConfigMap.
// In production, make a real Kubernetes API call here.
let map = &args[0];
assert_eq!(map["apiVersion"], "v1");
assert_eq!(map["kind"], "ConfigMap");
assert_eq!(map["name"], "my-params");
assert_eq!(map["namespace"], "default");
Ok(serde_json::json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": { "name": "my-params", "namespace": "default" },
"data": { "maxreplicas": "5" }
}))
})
.build()?
.eval(Some(&bindings.to_string()))?;
let result: serde_json::Value = serde_json::from_str(&result_str)?;
assert_eq!(result["accepted"], true); // replicas 3 <= maxreplicas 5
}