Expression language¶
Portal's default expression engine is expr-lang/expr via internal/expr/exprlang. The engine compiles each rule: expression once at load time and re-runs the compiled program per evaluation.
The engine interface is pluggable (api.ExpressionEngine) — CEL/Rego/starlark are drop-in candidates for v3 without changing the rule schema.
Bound env¶
The env keys exposed by the pod ContextBuilder (internal/context/pod/builder.go) are:
| Key | When | Type |
|---|---|---|
object |
always | nested map[string]any from unstructured.Unstructured |
metadata |
always | shortcut to object.metadata |
container |
pod-shaped GVKs only | one container per evaluation pass |
spec |
pod-shaped GVKs only | the pod-shape .spec projection |
securityContext |
pod-shaped GVKs only | the pod-level .spec.securityContext |
request |
admission only | {operation, dryRun, userInfo, oldObject} |
cluster.<gvk> |
always | informer-cache-backed accessor |
consistentCluster.<gvk> |
always | direct-API-call accessor |
Detailed coverage of the pod-sugar fields is in ../concepts/context-and-pod-sugar.md; the cross-resource helpers are in ../concepts/cross-resource.md.
Operators Portal rules lean on¶
| Operator | Example |
|---|---|
| Membership | container.image.registry in ["docker.io", "ghcr.io"] |
| Regex (operator form) | object.metadata.name matches "^web-" |
| Null-safe nav | container.securityContext?.privileged == true |
| Null coalesce | container.image.tag ?? "latest" |
| String prefixes/suffixes | startsWith(object.metadata.name, "web-"), endsWith(...) |
startsWith, endsWith, contains, and the matches operator are part of expr-lang's stdlib.
Custom helpers¶
| Helper | Signature | Notes |
|---|---|---|
regexMatch |
regexMatch(s, pattern string) bool |
Function-form alternative to the matches operator for when the operator form is awkward (e.g. dynamic pattern). Pattern cache is global. |
The helper is registered in internal/expr/exprlang/engine.go.
Result contract¶
Every rule: expression must evaluate to bool. A non-bool return surfaces as:
- a compile-time error (caught at load; written to
.status.parseError), or - an eval-time error (
expression did not produce bool, got %T) which the engine wraps aseval: ....
Errors at eval time mark the evaluation as failed and increment the error counters — they do not bubble up as a deny decision.
Worked snippets¶
# privileged or escalating
container.securityContext.privileged == true
|| container.securityContext.allowPrivilegeEscalation == true
# every Deployment must have a matching PDB
cluster.poddisruptionbudgets.list(
object.metadata.namespace,
object.spec.selector.matchLabels
) | len == 0
# block images outside an allow-list
not (container.image.registry in ["ghcr.io", "registry.k8s.io"])
# require a team label
not ("team" in keys(object.metadata.labels ?? {}))