Kubernetes Custom Resource Definitions (CRDs) are the foundation of platform engineering, allowing us to extend the Kubernetes API with our own declarative resources. But a well-defined API is more than just a schema; it’s a contract that guarantees integrity and enforces rules. While basic OpenAPI schema validation can define types and required fields, it falls short when faced with the complex, relational logic inherent in real-world applications.
Historically, enforcing rules like “all hostnames in this list must be unique” or “this field must be immutable after creation” required building, deploying, and maintaining a separate validating admission webhook. This introduced network latency, operational overhead, and a significant point of failure.
Today, there is a better way. By embedding Common Expression Language (CEL) rules directly into the CRD, we can perform fast, secure, and declarative validation inside the Kubernetes API server itself. This article is a deep dive into advanced CEL patterns for validating nested objects, lists, and stateful transitions, effectively replacing 90% of common webhook use cases.
The Power of In-Process Validation
CEL validation rules live within the x-kubernetes-validations
extension field of a CRD’s openAPIV3Schema
. The API server evaluates these rules after basic schema validation but before an object is persisted to etcd
.
Each rule contains:
rule
: A CEL expression that must evaluate totrue
.message
ormessageExpression
: A clear error returned to the user if the rule fails.
The expressions have access to self
(the incoming object) and, on updates, oldSelf
(the object’s existing state in etcd
). This simple foundation unlocks incredibly powerful validation capabilities.
Let’s build a practical example: a WebACL
CRD for a custom API gateway.
|
|
Pattern 1: Conditional Validation in Nested Objects
A common requirement is making a field required only if another field has a certain value. For our WebACL
, let’s say a rateLimit
object is only meaningful if the action
is block
.
First, define the schema for the spec
.
|
|
Now, we add a CEL rule at the spec
level to enforce the dependency.
|
|
The logic !A || B
is a classic way to express “if A, then B”. If a user tries to create a WebACL
with action: 'block'
but without a rateLimit
object, the API server will reject it instantly with our clear error message.
|
|
Pattern 2: Enforcing Uniqueness in Lists
Imagine our WebACL
can target multiple hostnames. It’s critical that this list contains no duplicates. Basic schema validation cannot enforce this. CEL’s list manipulation functions make it trivial.
Let’s add a hosts
list to our spec
.
|
|
To validate uniqueness, we add a rule that maps the list to itself and compares its size to the size of a deduplicated set.
|
|
We use the concise and highly readable isUnique()
macro. This is equivalent to the more explicit self.size() == self.toSet().size()
, which you may see in older examples. Submitting a resource with hosts: ["a.com", "b.com", "a.com"]
will now fail validation.
Pattern 3: Validating Fields Within a List of Objects
This is where CEL truly shines. Let’s add a list of rules
to our WebACL
, where each rule defines an IP block. We want to ensure that every cidr
in the list is a valid CIDR notation string.
First, the schema:
|
|
Now, we add a validation rule using the all()
macro, which iterates over each element in the list. CEL includes built-in functions like isCIDR()
.
|
|
If any cidr
field in the list is invalid, the entire object is rejected.
Pattern 4: Creating Immutable Fields with oldSelf
One of the most powerful features of cgo
is its ability to enforce transition rules—logic that depends on the object’s previous state. The oldSelf
variable makes this possible.
Let’s say we want to make the action
field of our WebACL
immutable. Once it’s set to allow
or block
, it cannot be changed.
|
|
This rule is the key to immutability:
- On
CREATE
,oldSelf
does not exist, so!has(oldSelf)
istrue
, and the rule passes. - On
UPDATE
,has(oldSelf)
istrue
, so the second part of the expression is evaluated:self.action == oldSelf.action
. If the user attempts to change the action, this evaluates tofalse
, and the update is rejected.
When to Use Webhooks: The Escape Hatch
CEL is not a silver bullet. Its power comes from its limitations. It is designed to be fast and safe, which means it cannot:
- Make network calls.
- Query the Kubernetes API for other objects.
If your validation logic requires checking state in another resource (e.g., “does the Secret specified in tls.secretName
actually exist?”), you still need to build a validating admission webhook. The best practice is to use CEL for all self-contained resource integrity and reserve webhooks for complex, cross-resource, or external system validation.
Conclusion
CEL-based validation represents a major leap forward for Kubernetes API extensions. By moving complex validation logic from brittle, high-latency webhooks into the CRD itself, we build more robust, secure, and operationally simple platforms. Mastering the patterns of conditional logic, list manipulation with all()
and isUnique()
, and stateful transitions with oldSelf
allows you to cover the vast majority of validation requirements declaratively. This not only improves the quality of your custom APIs but also provides operators with immediate, clear, and actionable feedback, creating a far superior user experience.