Skip to content

Commit

Permalink
✨ Introduce CEL for ClusterClass Variables (#9239)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
5 people committed Jun 27, 2024
1 parent 1e6896e commit ada2764
Show file tree
Hide file tree
Showing 26 changed files with 4,239 additions and 301 deletions.
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ issues:
- stylecheck
path: util/defaulting/defaulting.go
text: should not use dot imports
# Large parts of this file are duplicate from k/k. Let's ignore "emptyStringTest" to reduce the noise in diffs
# and to avoid making mistakes by diverging from upstream just because of this purely stylistic linter finding.
- linters:
- gocritic
text: "emptyStringTest"
path: internal/topology/variables/clusterclass_variable_validation.go
# Append should be able to assign to a different var/slice.
- linters:
- gocritic
Expand Down
130 changes: 129 additions & 1 deletion api/v1beta1/clusterclass_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ type VariableSchema struct {
OpenAPIV3Schema JSONSchemaProps `json:"openAPIV3Schema"`
}

// Adapted from https://github.com/kubernetes/apiextensions-apiserver/blob/v0.28.5/pkg/apis/apiextensions/v1/types_jsonschema.go#L40

// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
// This struct has been initially copied from apiextensionsv1.JSONSchemaProps, but all fields
// which are not supported in CAPI have been removed.
Expand Down Expand Up @@ -461,6 +463,16 @@ type JSONSchemaProps struct {
// +kubebuilder:validation:Schemaless
AdditionalProperties *JSONSchemaProps `json:"additionalProperties,omitempty"`

// MaxProperties is the maximum amount of entries in a map or properties in an object.
// NOTE: Can only be set if type is object.
// +optional
MaxProperties *int64 `json:"maxProperties,omitempty"`

// MinProperties is the minimum amount of entries in a map or properties in an object.
// NOTE: Can only be set if type is object.
// +optional
MinProperties *int64 `json:"minProperties,omitempty"`

// Required specifies which fields of an object are required.
// NOTE: Can only be set if type is object.
// +optional
Expand Down Expand Up @@ -551,7 +563,123 @@ type JSONSchemaProps struct {
// NOTE: Can be set for all types.
// +optional
Default *apiextensionsv1.JSON `json:"default,omitempty"`
}

// XValidations describes a list of validation rules written in the CEL expression language.
// +optional
// +listType=map
// +listMapKey=rule
XValidations []ValidationRule `json:"x-kubernetes-validations,omitempty"`
}

// ValidationRule describes a validation rule written in the CEL expression language.
type ValidationRule struct {
// Rule represents the expression which will be evaluated by CEL.
// ref: https://github.com/google/cel-spec
// The Rule is scoped to the location of the x-kubernetes-validations extension in the schema.
// The `self` variable in the CEL expression is bound to the scoped value.
// If the Rule is scoped to an object with properties, the accessible properties of the object are field selectable
// via `self.field` and field presence can be checked via `has(self.field)`.
// If the Rule is scoped to an object with additionalProperties (i.e. a map) the value of the map
// are accessible via `self[mapKey]`, map containment can be checked via `mapKey in self` and all entries of the map
// are accessible via CEL macros and functions such as `self.all(...)`.
// If the Rule is scoped to an array, the elements of the array are accessible via `self[i]` and also by macros and
// functions.
// If the Rule is scoped to a scalar, `self` is bound to the scalar value.
// Examples:
// - Rule scoped to a map of objects: {"rule": "self.components['Widget'].priority < 10"}
// - Rule scoped to a list of integers: {"rule": "self.values.all(value, value >= 0 && value < 100)"}
// - Rule scoped to a string value: {"rule": "self.startsWith('kube')"}
//
// Unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields is not accessible in CEL
// expressions. This includes:
// - Unknown field values that are preserved by object schemas with x-kubernetes-preserve-unknown-fields.
// - Object properties where the property schema is of an "unknown type". An "unknown type" is recursively defined as:
// - A schema with no type and x-kubernetes-preserve-unknown-fields set to true
// - An array where the items schema is of an "unknown type"
// - An object where the additionalProperties schema is of an "unknown type"
//
// Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
// Accessible property names are escaped according to the following rules when accessed in the expression:
// - '__' escapes to '__underscores__'
// - '.' escapes to '__dot__'
// - '-' escapes to '__dash__'
// - '/' escapes to '__slash__'
// - Property names that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are:
// "true", "false", "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if",
// "import", "let", "loop", "package", "namespace", "return".
// Examples:
// - Rule accessing a property named "namespace": {"rule": "self.__namespace__ > 0"}
// - Rule accessing a property named "x-prop": {"rule": "self.x__dash__prop > 0"}
// - Rule accessing a property named "redact__d": {"rule": "self.redact__underscores__d > 0"}
//
//
// If `rule` makes use of the `oldSelf` variable it is implicitly a
// `transition rule`.
//
// By default, the `oldSelf` variable is the same type as `self`.
//
// Transition rules by default are applied only on UPDATE requests and are
// skipped if an old value could not be found.
//
// +kubebuilder:validation:Required
Rule string `json:"rule"`
// Message represents the message displayed when validation fails. The message is required if the Rule contains
// line breaks. The message must not contain line breaks.
// If unset, the message is "failed rule: {Rule}".
// e.g. "must be a URL with the host matching spec.host"
// +optional
Message string `json:"message,omitempty"`
// MessageExpression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails.
// Since messageExpression is used as a failure message, it must evaluate to a string.
// If both message and messageExpression are present on a rule, then messageExpression will be used if validation
// fails. If messageExpression results in a runtime error, the validation failure message is produced
// as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string
// that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset.
// messageExpression has access to all the same variables as the rule; the only difference is the return type.
// Example:
// "x must be less than max ("+string(self.max)+")"
// +optional
MessageExpression string `json:"messageExpression,omitempty"`
// Reason provides a machine-readable validation failure reason that is returned to the caller when a request fails this validation rule.
// The currently supported reasons are: "FieldValueInvalid", "FieldValueForbidden", "FieldValueRequired", "FieldValueDuplicate".
// If not set, default to use "FieldValueInvalid".
// All future added reasons must be accepted by clients when reading this value and unknown reasons should be treated as FieldValueInvalid.
// +optional
// +kubebuilder:validation:Enum=FieldValueInvalid;FieldValueForbidden;FieldValueRequired;FieldValueDuplicate
// +kubebuilder:default=FieldValueInvalid
// +default=ref(sigs.k8s.io/cluster-api/api/v1beta1.FieldValueInvalid)
Reason FieldValueErrorReason `json:"reason,omitempty"`
// FieldPath represents the field path returned when the validation fails.
// 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.
// e.g. when validation checks if a specific attribute `foo` under a map `testMap`, the fieldPath could be set to `.testMap.foo`
// If the validation checks two lists must have unique attributes, the fieldPath could be set to either of the list: e.g. `.testList`
// It does not support list numeric index.
// 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.
// Numeric index of array is not supported.
// For field name which contains special characters, use `['specialName']` to refer the field name.
// e.g. for attribute `foo.34$` appears in a list `testList`, the fieldPath could be set to `.testList['foo.34$']`
// +optional
FieldPath string `json:"fieldPath,omitempty"`
}

// FieldValueErrorReason is a machine-readable value providing more detail about why a field failed the validation.
type FieldValueErrorReason string

const (
// FieldValueRequired is used to report required values that are not
// provided (e.g. empty strings, null values, or empty arrays).
FieldValueRequired FieldValueErrorReason = "FieldValueRequired"
// FieldValueDuplicate is used to report collisions of values that must be
// unique (e.g. unique IDs).
FieldValueDuplicate FieldValueErrorReason = "FieldValueDuplicate"
// FieldValueInvalid is used to report malformed values (e.g. failed regex
// match, too long, out of bounds).
FieldValueInvalid FieldValueErrorReason = "FieldValueInvalid"
// FieldValueForbidden is used to report valid (as per formatting rules)
// values which would be accepted under some conditions, but which are not
// permitted by the current conditions (such as security policy).
FieldValueForbidden FieldValueErrorReason = "FieldValueForbidden"
)

// ClusterClassPatch defines a patch which is applied to customize the referenced templates.
type ClusterClassPatch struct {
Expand Down
30 changes: 30 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ada2764

Please sign in to comment.