Skip to content

Improve error messages for CEL validation #132528

Open
@fabriziopandini

Description

@fabriziopandini

What would you like to be added?

When I use CEL validation as of today I get messages that not easy to parse for users, e.g.

* spec.name: Invalid value: "string": name must consist of lower case alphanumeric characters
* spec.pods: Invalid value: "array": entries in pods must be unique

More specifically, looks “string” or "array" is not reporting the actual value of the field but the field type.

This can be reproduced by creating a kind cluster (I used K8s v1.32.0), applying the following CRD:

---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.18.0
  name: testresources.test.cluster.x-k8s.io
spec:
  group: test.cluster.x-k8s.io
  names:
    kind: TestResource
    listKind: TestResourceList
    plural: testresources
    singular: testresource
  scope: Namespaced
  versions:
  - name: v1beta1
    schema:
      openAPIV3Schema:
        description: TestResource defines a test resource.
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: TestResourceSpec defines the resource spec.
            properties:
              name:
                maxLength: 253
                minLength: 1
                type: string
                x-kubernetes-validations:
                - message: name must consist of lower case alphanumeric characters
                  rule: self.matches('^[a-z0-9]*$')
              pods:
                items:
                  maxLength: 256
                  minLength: 1
                  type: string
                maxItems: 32
                minItems: 1
                type: array
                x-kubernetes-list-type: atomic
                x-kubernetes-validations:
                - message: entries in pods must be unique
                  rule: self.all(x, self.exists_one(y, x == y))
            required:
            - name
            type: object
          status:
            description: TestResourceStatus defines the status of a TestResource.
            properties:
              availableReplicas:
                format: int32
                type: integer
              readyReplicas:
                format: int32
                type: integer
              replicas:
                format: int32
                type: integer
              upToDateReplicas:
                format: int32
                type: integer
              version:
                maxLength: 256
                minLength: 1
                type: string
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}

And then the following yaml to create an invalid resource

apiVersion: test.cluster.x-k8s.io/v1beta1
kind: TestResource
metadata:
  name: invalid
spec:
  name: $__
  pods:
    - foo
    - foo

FYI there was some discussion in https://kubernetes.slack.com/archives/C02TTBG6LF4/p1750091061651119 about this issue, capturing here some relevant points:

@sbueringer I would probably expect from a user pov that no message prefix is added if message or messageExpression is set

@erikgb
From a developer POV I think it is nice to get some context automatically injected into the error message. 😉 But I would prefer the invalid value to be echoed, and not the type. So I agree there is a bug somewhere.

@JoelSpeed
When you add an XValidation, you can specify the type of the error, whether it's Invalid, Forbidden etc, that is where this context is coming from, I wonder if this is specifically an issue with Invalid, or whether the same also applies to Forbidden or other error types

@liggitt
maybe something like this would work

diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go
index 575fd5e2e9a..2a3f7ca29df 100644
--- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go
+++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go
@@ -476,15 +476,16 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path
 						return errs, -1
 					} else {
 						klog.V(2).ErrorS(msgErr, "messageExpression evaluation failed")
-						addErr(fieldErrorForReason(currentFldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason))
+						addErr(fieldErrorForReason(currentFldPath, obj, ruleMessageOrDefault(rule), rule.Reason))
 						remainingBudget = newRemainingBudget
 					}
 				} else {
-					addErr(fieldErrorForReason(currentFldPath, sts.Type, messageExpression, rule.Reason))
+					// messageExpression is expected to embed the value if desired
+					addErr(fieldErrorForReason(currentFldPath, field.OmitValueType{}, messageExpression, rule.Reason))
 					remainingBudget = newRemainingBudget
 				}
 			} else {
-				addErr(fieldErrorForReason(currentFldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason))
+				addErr(fieldErrorForReason(currentFldPath, obj, ruleMessageOrDefault(rule), rule.Reason))
 			}
 		}
 	}
@@ -675,7 +676,13 @@ func fieldErrorForReason(fldPath *field.Path, value interface{}, detail string,
 	case apiextensions.FieldValueDuplicate:
 		return field.Duplicate(fldPath, value)
 	default:
-		return field.Invalid(fldPath, value, detail)
+		displayValue := value
+		switch value.(type) {
+		case map[string]any, []any:
+			// avoid outputting complex structured field values
+			displayValue = field.OmitValueType{}
+		}
+		return field.Invalid(fldPath, displayValue, detail)
 	}
 }

needs good tests and such

/sig api-machinery /cc @liggitt

Why is this needed?

Improve UX when CRD implements validation rules using CEL

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/featureCategorizes issue or PR as related to a new feature.needs-triageIndicates an issue or PR lacks a `triage/foo` label and requires one.sig/api-machineryCategorizes an issue or PR as relevant to SIG API Machinery.

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions