Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
}

Required Bindings

BindingRequired when…
objectAlways (the resource being admitted)
oldObjectPolicy expressions reference oldObject
requestPolicy expressions reference request
namespaceObjectPolicy expressions reference namespaceObject
paramRefparamKind is set (see below)

object, oldObject, and request correspond directly to the fields of the Kubernetes AdmissionReview request object.

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.k8s API 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:

KeySet by chain stepNotes
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
}