Skip to content

Commit ada2764

Browse files
chaunceyjiangjimmidysonsbueringer
authored
✨ Introduce CEL for ClusterClass Variables (#9239)
* Introduce CEL for ClusterClass Variables Signed-off-by: chaunceyjiang <[email protected]> * feat: Implement CEL validation * refactor: Add comments from previous code reviews * chore: Generate CC manifest after fixing list type annotation (#2) * chore: Fix up CRD manifest * fix: Pass through context to CEL funcs * feat: Add CEL admission cost validation * refactor: Add nolint to unbounded * refactor: Fix up new func signature * build: Fix up go mod for tools * fixup! refactor: Apply review feedback * fixup! build: Regenerate openapi spec * fixup! refactor: Apply review feedback * fixup! fix: Regenerate everything * fixup! fix: Apply review feedback * fixup! fix: More review feedback * fixup! refactor: Address review feedback, especially re recursion * fixup! fix: Check total cost * fixup! refactor: Address review feedback - rename testCtx to ctx * CEL: Various improvements (#3) * resolve compile issue after rebase * Some more improvements (#4) --------- Signed-off-by: chaunceyjiang <[email protected]> Co-authored-by: Jimmi Dyson <[email protected]> Co-authored-by: Jimmi Dyson <[email protected]> Co-authored-by: Stefan Büringer <[email protected]> Co-authored-by: Stefan Bueringer <[email protected]>
1 parent 1e6896e commit ada2764

26 files changed

+4239
-301
lines changed

.golangci.yml

+6
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ issues:
290290
- stylecheck
291291
path: util/defaulting/defaulting.go
292292
text: should not use dot imports
293+
# Large parts of this file are duplicate from k/k. Let's ignore "emptyStringTest" to reduce the noise in diffs
294+
# and to avoid making mistakes by diverging from upstream just because of this purely stylistic linter finding.
295+
- linters:
296+
- gocritic
297+
text: "emptyStringTest"
298+
path: internal/topology/variables/clusterclass_variable_validation.go
293299
# Append should be able to assign to a different var/slice.
294300
- linters:
295301
- gocritic

api/v1beta1/clusterclass_types.go

+129-1
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ type VariableSchema struct {
427427
OpenAPIV3Schema JSONSchemaProps `json:"openAPIV3Schema"`
428428
}
429429

430+
// Adapted from https://github.com/kubernetes/apiextensions-apiserver/blob/v0.28.5/pkg/apis/apiextensions/v1/types_jsonschema.go#L40
431+
430432
// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
431433
// This struct has been initially copied from apiextensionsv1.JSONSchemaProps, but all fields
432434
// which are not supported in CAPI have been removed.
@@ -461,6 +463,16 @@ type JSONSchemaProps struct {
461463
// +kubebuilder:validation:Schemaless
462464
AdditionalProperties *JSONSchemaProps `json:"additionalProperties,omitempty"`
463465

466+
// MaxProperties is the maximum amount of entries in a map or properties in an object.
467+
// NOTE: Can only be set if type is object.
468+
// +optional
469+
MaxProperties *int64 `json:"maxProperties,omitempty"`
470+
471+
// MinProperties is the minimum amount of entries in a map or properties in an object.
472+
// NOTE: Can only be set if type is object.
473+
// +optional
474+
MinProperties *int64 `json:"minProperties,omitempty"`
475+
464476
// Required specifies which fields of an object are required.
465477
// NOTE: Can only be set if type is object.
466478
// +optional
@@ -551,7 +563,123 @@ type JSONSchemaProps struct {
551563
// NOTE: Can be set for all types.
552564
// +optional
553565
Default *apiextensionsv1.JSON `json:"default,omitempty"`
554-
}
566+
567+
// XValidations describes a list of validation rules written in the CEL expression language.
568+
// +optional
569+
// +listType=map
570+
// +listMapKey=rule
571+
XValidations []ValidationRule `json:"x-kubernetes-validations,omitempty"`
572+
}
573+
574+
// ValidationRule describes a validation rule written in the CEL expression language.
575+
type ValidationRule struct {
576+
// Rule represents the expression which will be evaluated by CEL.
577+
// ref: https://github.com/google/cel-spec
578+
// The Rule is scoped to the location of the x-kubernetes-validations extension in the schema.
579+
// The `self` variable in the CEL expression is bound to the scoped value.
580+
// If the Rule is scoped to an object with properties, the accessible properties of the object are field selectable
581+
// via `self.field` and field presence can be checked via `has(self.field)`.
582+
// If the Rule is scoped to an object with additionalProperties (i.e. a map) the value of the map
583+
// are accessible via `self[mapKey]`, map containment can be checked via `mapKey in self` and all entries of the map
584+
// are accessible via CEL macros and functions such as `self.all(...)`.
585+
// If the Rule is scoped to an array, the elements of the array are accessible via `self[i]` and also by macros and
586+
// functions.
587+
// If the Rule is scoped to a scalar, `self` is bound to the scalar value.
588+
// Examples:
589+
// - Rule scoped to a map of objects: {"rule": "self.components['Widget'].priority < 10"}
590+
// - Rule scoped to a list of integers: {"rule": "self.values.all(value, value >= 0 && value < 100)"}
591+
// - Rule scoped to a string value: {"rule": "self.startsWith('kube')"}
592+
//
593+
// Unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields is not accessible in CEL
594+
// expressions. This includes:
595+
// - Unknown field values that are preserved by object schemas with x-kubernetes-preserve-unknown-fields.
596+
// - Object properties where the property schema is of an "unknown type". An "unknown type" is recursively defined as:
597+
// - A schema with no type and x-kubernetes-preserve-unknown-fields set to true
598+
// - An array where the items schema is of an "unknown type"
599+
// - An object where the additionalProperties schema is of an "unknown type"
600+
//
601+
// Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
602+
// Accessible property names are escaped according to the following rules when accessed in the expression:
603+
// - '__' escapes to '__underscores__'
604+
// - '.' escapes to '__dot__'
605+
// - '-' escapes to '__dash__'
606+
// - '/' escapes to '__slash__'
607+
// - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:
608+
// "true", "false", "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if",
609+
// "import", "let", "loop", "package", "namespace", "return".
610+
// Examples:
611+
// - Rule accessing a property named "namespace": {"rule": "self.__namespace__ > 0"}
612+
// - Rule accessing a property named "x-prop": {"rule": "self.x__dash__prop > 0"}
613+
// - Rule accessing a property named "redact__d": {"rule": "self.redact__underscores__d > 0"}
614+
//
615+
//
616+
// If `rule` makes use of the `oldSelf` variable it is implicitly a
617+
// `transition rule`.
618+
//
619+
// By default, the `oldSelf` variable is the same type as `self`.
620+
//
621+
// Transition rules by default are applied only on UPDATE requests and are
622+
// skipped if an old value could not be found.
623+
//
624+
// +kubebuilder:validation:Required
625+
Rule string `json:"rule"`
626+
// Message represents the message displayed when validation fails. The message is required if the Rule contains
627+
// line breaks. The message must not contain line breaks.
628+
// If unset, the message is "failed rule: {Rule}".
629+
// e.g. "must be a URL with the host matching spec.host"
630+
// +optional
631+
Message string `json:"message,omitempty"`
632+
// MessageExpression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails.
633+
// Since messageExpression is used as a failure message, it must evaluate to a string.
634+
// If both message and messageExpression are present on a rule, then messageExpression will be used if validation
635+
// fails. If messageExpression results in a runtime error, the validation failure message is produced
636+
// as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string
637+
// that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset.
638+
// messageExpression has access to all the same variables as the rule; the only difference is the return type.
639+
// Example:
640+
// "x must be less than max ("+string(self.max)+")"
641+
// +optional
642+
MessageExpression string `json:"messageExpression,omitempty"`
643+
// Reason provides a machine-readable validation failure reason that is returned to the caller when a request fails this validation rule.
644+
// The currently supported reasons are: "FieldValueInvalid", "FieldValueForbidden", "FieldValueRequired", "FieldValueDuplicate".
645+
// If not set, default to use "FieldValueInvalid".
646+
// All future added reasons must be accepted by clients when reading this value and unknown reasons should be treated as FieldValueInvalid.
647+
// +optional
648+
// +kubebuilder:validation:Enum=FieldValueInvalid;FieldValueForbidden;FieldValueRequired;FieldValueDuplicate
649+
// +kubebuilder:default=FieldValueInvalid
650+
// +default=ref(sigs.k8s.io/cluster-api/api/v1beta1.FieldValueInvalid)
651+
Reason FieldValueErrorReason `json:"reason,omitempty"`
652+
// FieldPath represents the field path returned when the validation fails.
653+
// It must be a relative JSON path (i.e. with array notation) scoped to the location of this x-kubernetes-validations extension in the schema and refer to an existing field.
654+
// e.g. when validation checks if a specific attribute `foo` under a map `testMap`, the fieldPath could be set to `.testMap.foo`
655+
// If the validation checks two lists must have unique attributes, the fieldPath could be set to either of the list: e.g. `.testList`
656+
// It does not support list numeric index.
657+
// It supports child operation to refer to an existing field currently. Refer to [JSONPath support in Kubernetes](https://kubernetes.io/docs/reference/kubectl/jsonpath/) for more info.
658+
// Numeric index of array is not supported.
659+
// For field name which contains special characters, use `['specialName']` to refer the field name.
660+
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
661+
// +optional
662+
FieldPath string `json:"fieldPath,omitempty"`
663+
}
664+
665+
// FieldValueErrorReason is a machine-readable value providing more detail about why a field failed the validation.
666+
type FieldValueErrorReason string
667+
668+
const (
669+
// FieldValueRequired is used to report required values that are not
670+
// provided (e.g. empty strings, null values, or empty arrays).
671+
FieldValueRequired FieldValueErrorReason = "FieldValueRequired"
672+
// FieldValueDuplicate is used to report collisions of values that must be
673+
// unique (e.g. unique IDs).
674+
FieldValueDuplicate FieldValueErrorReason = "FieldValueDuplicate"
675+
// FieldValueInvalid is used to report malformed values (e.g. failed regex
676+
// match, too long, out of bounds).
677+
FieldValueInvalid FieldValueErrorReason = "FieldValueInvalid"
678+
// FieldValueForbidden is used to report valid (as per formatting rules)
679+
// values which would be accepted under some conditions, but which are not
680+
// permitted by the current conditions (such as security policy).
681+
FieldValueForbidden FieldValueErrorReason = "FieldValueForbidden"
682+
)
555683

556684
// ClusterClassPatch defines a patch which is applied to customize the referenced templates.
557685
type ClusterClassPatch struct {

api/v1beta1/zz_generated.deepcopy.go

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)