adllm Insights logo adllm Insights logo

Advanced CRD Validation with CEL for Nested and List Objects

Published on by The adllm Team. Last modified: . Tags: kubernetes crd cel api validation operator-sdk

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 to true.
  • message or messageExpression: 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Our base CRD structure
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: webacls.networking.example.com
spec:
  group: networking.example.com
  names:
    kind: WebACL
    plural: webacls
    singular: webacl
  scope: Namespaced
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                # Our fields to validate go here...

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ... spec.versions.schema.openAPIV3Schema.properties.spec
properties:
  action:
    type: string
    enum: ["allow", "block"]
  rateLimit:
    type: object
    properties:
      requestsPerMinute:
        type: integer
        minimum: 1
      burst:
        type: integer
        minimum: 0

Now, we add a CEL rule at the spec level to enforce the dependency.

1
2
3
4
5
6
7
8
9
# ... spec.versions.schema.openAPIV3Schema.properties.spec
type: object
x-kubernetes-validations:
  # Rule: if action is 'block', rateLimit must be defined.
  - rule: "self.action != 'block' || has(self.rateLimit)"
    message: "rateLimit settings must be provided when action is 'block'"
properties:
  action:
  # ...

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.

1
2
3
4
# kubectl apply -f invalid-acl.yaml --dry-run=server
Error from server (BadRequest): error when creating "invalid-acl.yaml":
WebACL.networking.example.com "my-acl" is invalid:
spec: Invalid value: "object": rateLimit settings must be provided when action is 'block'

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.

1
2
3
4
5
6
7
# ... spec.versions.schema.openAPIV3Schema.properties.spec.properties
hosts:
  type: array
  items:
    type: string
    format: hostname
  minItems: 1

To validate uniqueness, we add a rule that maps the list to itself and compares its size to the size of a deduplicated set.

1
2
3
4
5
6
7
8
9
# ... spec.versions.schema.openAPIV3Schema.properties.spec.properties.hosts
type: array
x-kubernetes-validations:
  # Rule: all items in the hosts list must be unique.
  - rule: "self.isUnique()"
    message: "All hostnames in the list must be unique"
items:
  type: string
# ...

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ... spec.versions.schema.openAPIV3Schema.properties.spec.properties
rules:
  type: array
  items:
    type: object
    required: ["name", "cidr"]
    properties:
      name:
        type: string
      cidr:
        type: string

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().

1
2
3
4
5
6
7
8
# ... spec.versions.schema.openAPIV3Schema.properties.spec.properties.rules
type: array
x-kubernetes-validations:
  # Rule: iterate over every rule `r` in the list.
  - rule: "self.all(r, isCIDR(r.cidr))"
    message: "Each rule must have a valid CIDR address"
items:
# ...

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# ... spec.versions.schema.openAPIV3Schema.properties.spec
type: object
x-kubernetes-validations:
  - rule: "self.action != 'block' || has(self.rateLimit)"
    message: "rateLimit settings must be provided when action is 'block'"
  # Rule: On UPDATE, the new action must equal the old action.
  # The `!has(oldSelf)` part handles the CREATE case.
  - rule: "!has(oldSelf) || self.action == oldSelf.action"
    message: "The 'action' field is immutable and cannot be changed"
properties:
# ...

This rule is the key to immutability:

  • On CREATE, oldSelf does not exist, so !has(oldSelf) is true, and the rule passes.
  • On UPDATE, has(oldSelf) is true, so the second part of the expression is evaluated: self.action == oldSelf.action. If the user attempts to change the action, this evaluates to false, 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.