Kubernetes ValidatingAdmissionPolicy Support
ferricel can compile a Kubernetes
ValidatingAdmissionPolicy
(VAP) into a self-contained WebAssembly module. The compiled module fetches
Kubernetes data it needs — the namespace object and param resources —
itself, through host extensions. The host supplies everything else (the
resource under admission, the admission request) via bindings.
This is a ferricel-core library feature. Enable it with the k8s-vap Cargo
feature.
Response Shape
The module’s evaluate export returns a JSON object.
Request accepted:
{ "accepted": true }
Or, on rejection:
{ "accepted": false, "message": "too many replicas", "code": 422 }
The message field comes from the failing validation’s message field, or from
its messageExpression if one is specified. If neither is set, a default
message is generated from the validation expression text. If the
messageExpression produces a runtime error or a non-string value, the module
falls back to the message field (or the default message), as Kubernetes does.
The code field is derived from the validation’s reason field:
reason | HTTP code |
|---|---|
Forbidden | 403 |
Unauthorized | 401 |
RequestEntityTooLarge | 413 |
Invalid or unset | 422 |
Both responses can carry an optional warnings list of strings. The list is
present only under failurePolicy: Ignore, and only when the module skipped
at least one expression. The module skips an expression when it evaluates to
a runtime error. See failurePolicy: Ignore.
Evaluation Order
The compiled module follows the evaluation order of the Kubernetes VAP validator:
-
namespaceObject(only when the policy references it). The module resolves the Namespace of the resource under admission fromrequest. See Namespace object for the rules. -
params(only whenparamKindis set). The module resolves the list of param resources fromparamRef. See Params for the rules. WithoutparamKind, the module evaluates the steps below once, with noparamsbinding. -
For each param resource:
-
matchConditions. The module evaluates every condition in declaration order first. If any evaluates tofalse, this param does not apply to the request. Only afalseresult skips the param; a non-boolean result, like Kubernetes, counts as a match. Afalseresult wins even when another condition in the same param evaluates to a runtime error. Only when no condition isfalsedoesfailurePolicydecide what an error means (see Runtime Errors). The module skips the remainingmatchConditions, thevariables, and thevalidationsfor this param, and moves to the next param. A skip is not a rejection.paramsandnamespaceObjectare both available inmatchConditions. -
variables. Evaluated in declaration order. Each result is stored undervariables.<name>. It is accessible to latervariablesexpressions and to allvalidations. The module rebuilds thevariablesmap for each param. -
validations. Evaluated in declaration order. The first expression that does not evaluate totruemakes the module return a rejection response. A non-boolean result, for example a string ornull, is a rejection, like Kubernetes. The module does not evaluate the remaining validations or the remaining params.
-
-
When no param produced a rejection, the module returns
{"accepted": true}. An empty param list also produces this response.
When several params match a paramRef.selector and more than one rejects
the request, the module returns the first rejection only. Kubernetes
aggregates every rejection message. See LIMITATIONS.md.
Runtime Errors
A CEL expression can evaluate to a runtime error. Examples: division by
zero, a missing field, an unbound variable, or a host extension (such as
kw.k8s) that returns an error.
The host passes the failurePolicy of the policy in the failurePolicy
binding (see Required Bindings). The
value is "Fail" or "Ignore". A missing or null binding means Fail. If
the value is anything else, the module traps with a runtime error before the
first expression runs. The module does not read spec.failurePolicy from the
VAP. The host owns that value.
failurePolicy: Fail
When a validations expression evaluates to a runtime error, the module
does not return {"accepted": true} or a rejection. The module traps,
exactly like a plain CEL module does.
A matchConditions expression works the same way, but only when no
condition in the same param is false. A false result always skips the
param, even when another condition in the same param errors. This matches
Kubernetes: a false match condition wins over a runtime error.
When the module traps, the call to evaluate fails, and Engine::eval()
returns Err. The error downcasts to ferricel_core::CelRuntimeError. The
host denies the request.
Engine::eval() can also fail for other reasons: an epoch-deadline
interrupt, a memory limit, a Wasm trap, or a bug in a host extension. These
errors do not downcast to CelRuntimeError, so the host can tell the two
kinds apart. See
Runtime Errors and failurePolicy
for the host-side code.
failurePolicy: Ignore
The module applies the policy to each expression on its own, the same way Kubernetes does:
- When a
validationsexpression evaluates to an error, the module skips it. The next validation runs. Afalseresult from any other validation rejects the request. - When a
matchConditionsexpression evaluates to an error, and no condition in the same param isfalse, the module skips the current param, the same as for afalseresult. WithparamKind, the next param runs. WithoutparamKind, the policy does not apply, and the module accepts the request. Afalseresult in another condition of the same param wins over the error, and the module records no warning for it.
Each skipped expression adds one entry to the warnings list of the
response. The list is present on both accept and reject responses. It is
absent when the module skipped nothing:
{"accepted": true, "warnings": ["The module skipped validation[0] because the expression evaluated to an error (failurePolicy is Ignore): no such key: 'count'"]}
{"accepted": false, "message": "...", "code": 422, "warnings": ["The module skipped validation[0] because the expression evaluated to an error (failurePolicy is Ignore): no such key: 'count'"]}
The text names the expression (validation[<i>] or
matchCondition '<name>') and carries the error message. With paramKind,
the text names the param (params <namespace>/<name>), so an operator can
tell the params apart. Kubernetes sends no client warning for a skipped
expression. It records a metric and an audit annotation instead. The
warnings are a ferricel addition.
Ignore does not cover the params and namespaceObject lookups. An error
there always traps (see below). See
LIMITATIONS.md.
Errors that are never errors
Two cases are not runtime errors under either policy:
- A
variablesentry that evaluates to an error is stored as-is. The error propagates only into the expressions that referencevariables.<name>. An erroring variable that no expression uses is harmless. This matches the lazy evaluation of variables in Kubernetes. - Errors absorbed by CEL short-circuit operators are not errors. For example,
(1 / 0) == 1 || trueevaluates totrue.
Lookups that always trap
The params lookup traps before any matchConditions or validations
run, under both Fail and Ignore. If the host’s kw.k8s extension fails, or the lookup finds no
resource, and paramRef.parameterNotFoundAction is not Allow, the module
traps. For a host error, the origin field of the CelRuntimeError is
kw.k8s.get (for paramRef.name) or kw.k8s.list (for
paramRef.selector). For an empty result, the message is
no parameters found and origin is None. See
Params.
The namespaceObject lookup traps before the params lookup, and before
any matchConditions or validations run, when the request binding is
missing or the host’s kw.k8s.get call fails. For a host error, origin is
kw.k8s.get — the same origin a failed paramRef.name lookup uses, so a
host that only checks origin == kw.k8s.get cannot tell the two apart from
the error alone. Unlike params, there is no parameterNotFoundAction
equivalent: a cluster-scoped request, or a request for the Namespace
resource itself, does not trap — it makes namespaceObject null without
calling the host at all. See
Namespace object.
Known Limitations
The following VAP features are not yet implemented or are not part of ferricel’s scope:
| Feature | Status | Notes |
|---|---|---|
failurePolicy | Implemented | Via the failurePolicy binding (see Runtime Errors). Under Ignore, the module skips an expression that evaluates to an error and records a warning. |
auditAnnotations | Not implemented | Requires a separate compilation path and an additional field in the response JSON. |
matchConstraints | Out of scope | This is a server-side filter applied by the API server, not a CEL expression. The compiled module does not enforce it. |