mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 06:27:05 +00:00
Add validation rule compilation and validation of x-kubernetes-validations extension fields
This commit is contained in:
parent
34ccd3038b
commit
66af4ecfd5
@ -419,6 +419,7 @@ API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiexten
|
|||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XListType
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XListType
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XMapType
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XMapType
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XPreserveUnknownFields
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XPreserveUnknownFields
|
||||||
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XValidations
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaPropsOrArray,JSONSchemas
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaPropsOrArray,JSONSchemas
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaPropsOrArray,Schema
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaPropsOrArray,Schema
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaPropsOrBool,Allows
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaPropsOrBool,Allows
|
||||||
@ -435,6 +436,7 @@ API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiexten
|
|||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XListType
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XListType
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XMapType
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XMapType
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XPreserveUnknownFields
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XPreserveUnknownFields
|
||||||
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XValidations
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,JSONSchemas
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,JSONSchemas
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,Schema
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,Schema
|
||||||
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrBool,Allows
|
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrBool,Allows
|
||||||
|
@ -122,6 +122,80 @@ type JSONSchemaProps struct {
|
|||||||
// Atomic maps will be entirely replaced when updated.
|
// Atomic maps will be entirely replaced when updated.
|
||||||
// +optional
|
// +optional
|
||||||
XMapType *string
|
XMapType *string
|
||||||
|
|
||||||
|
// x-kubernetes-validations -kubernetes-validations describes a list of validation rules written in the CEL expression language.
|
||||||
|
// This field is an alpha-level. Using this field requires the feature gate `CustomResourceValidationExpressions` to be enabled.
|
||||||
|
// +patchMergeKey=rule
|
||||||
|
// +patchStrategy=merge
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=rule
|
||||||
|
XValidations ValidationRules
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationRules describes a list of validation rules written in the CEL expression language.
|
||||||
|
type ValidationRules []ValidationRule
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Example:
|
||||||
|
// - Rule scoped to the root of a resource with a status subresource: {"rule": "self.status.actual <= self.spec.maxDesired"}
|
||||||
|
//
|
||||||
|
// 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)`. Null valued fields are treated as
|
||||||
|
// absent fields in CEL expressions.
|
||||||
|
// 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')"}
|
||||||
|
//
|
||||||
|
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||||
|
// object and from any x-kubernetes-embedded-resource annotated objects. No other metadata properties are accessible.
|
||||||
|
//
|
||||||
|
// 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"}
|
||||||
|
//
|
||||||
|
// Equality on arrays with x-kubernetes-list-type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1].
|
||||||
|
// Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:
|
||||||
|
// - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and
|
||||||
|
// non-intersecting elements in `Y` are appended, retaining their partial order.
|
||||||
|
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
||||||
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
||||||
|
// non-intersecting keys are appended, retaining their partial order.
|
||||||
|
Rule string
|
||||||
|
// 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"
|
||||||
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON represents any valid JSON value.
|
// JSON represents any valid JSON value.
|
||||||
|
@ -161,6 +161,80 @@ type JSONSchemaProps struct {
|
|||||||
// Atomic maps will be entirely replaced when updated.
|
// Atomic maps will be entirely replaced when updated.
|
||||||
// +optional
|
// +optional
|
||||||
XMapType *string `json:"x-kubernetes-map-type,omitempty" protobuf:"bytes,43,opt,name=xKubernetesMapType"`
|
XMapType *string `json:"x-kubernetes-map-type,omitempty" protobuf:"bytes,43,opt,name=xKubernetesMapType"`
|
||||||
|
|
||||||
|
// x-kubernetes-validations describes a list of validation rules written in the CEL expression language.
|
||||||
|
// This field is an alpha-level. Using this field requires the feature gate `CustomResourceValidationExpressions` to be enabled.
|
||||||
|
// +patchMergeKey=rule
|
||||||
|
// +patchStrategy=merge
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=rule
|
||||||
|
XValidations ValidationRules `json:"x-kubernetes-validations,omitempty" patchStrategy:"merge" patchMergeKey:"rule" protobuf:"bytes,44,rep,name=xKubernetesValidations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationRules describes a list of validation rules written in the CEL expression language.
|
||||||
|
type ValidationRules []ValidationRule
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Example:
|
||||||
|
// - Rule scoped to the root of a resource with a status subresource: {"rule": "self.status.actual <= self.spec.maxDesired"}
|
||||||
|
//
|
||||||
|
// 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)`. Null valued fields are treated as
|
||||||
|
// absent fields in CEL expressions.
|
||||||
|
// 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')"}
|
||||||
|
//
|
||||||
|
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||||
|
// object and from any x-kubernetes-embedded-resource annotated objects. No other metadata properties are accessible.
|
||||||
|
//
|
||||||
|
// 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"}
|
||||||
|
//
|
||||||
|
// Equality on arrays with x-kubernetes-list-type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1].
|
||||||
|
// Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:
|
||||||
|
// - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and
|
||||||
|
// non-intersecting elements in `Y` are appended, retaining their partial order.
|
||||||
|
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
||||||
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
||||||
|
// non-intersecting keys are appended, retaining their partial order.
|
||||||
|
Rule string `json:"rule" protobuf:"bytes,1,opt,name=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"
|
||||||
|
Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON represents any valid JSON value.
|
// JSON represents any valid JSON value.
|
||||||
|
@ -161,6 +161,80 @@ type JSONSchemaProps struct {
|
|||||||
// Atomic maps will be entirely replaced when updated.
|
// Atomic maps will be entirely replaced when updated.
|
||||||
// +optional
|
// +optional
|
||||||
XMapType *string `json:"x-kubernetes-map-type,omitempty" protobuf:"bytes,43,opt,name=xKubernetesMapType"`
|
XMapType *string `json:"x-kubernetes-map-type,omitempty" protobuf:"bytes,43,opt,name=xKubernetesMapType"`
|
||||||
|
|
||||||
|
// x-kubernetes-validations describes a list of validation rules written in the CEL expression language.
|
||||||
|
// This field is an alpha-level. Using this field requires the feature gate `CustomResourceValidationExpressions` to be enabled.
|
||||||
|
// +patchMergeKey=rule
|
||||||
|
// +patchStrategy=merge
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=rule
|
||||||
|
XValidations ValidationRules `json:"x-kubernetes-validations,omitempty" patchStrategy:"merge" patchMergeKey:"rule" protobuf:"bytes,44,rep,name=xKubernetesValidations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidationRules describes a list of validation rules written in the CEL expression language.
|
||||||
|
type ValidationRules []ValidationRule
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// Example:
|
||||||
|
// - Rule scoped to the root of a resource with a status subresource: {"rule": "self.status.actual <= self.spec.maxDesired"}
|
||||||
|
//
|
||||||
|
// 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)`. Null valued fields are treated as
|
||||||
|
// absent fields in CEL expressions.
|
||||||
|
// 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')"}
|
||||||
|
//
|
||||||
|
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||||
|
// object and from any x-kubernetes-embedded-resource annotated objects. No other metadata properties are accessible.
|
||||||
|
//
|
||||||
|
// 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"}
|
||||||
|
//
|
||||||
|
// Equality on arrays with x-kubernetes-list-type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1].
|
||||||
|
// Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type:
|
||||||
|
// - 'set': `X + Y` performs a union where the array positions of all elements in `X` are preserved and
|
||||||
|
// non-intersecting elements in `Y` are appended, retaining their partial order.
|
||||||
|
// - 'map': `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
|
||||||
|
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
|
||||||
|
// non-intersecting keys are appended, retaining their partial order.
|
||||||
|
Rule string `json:"rule" protobuf:"bytes,1,opt,name=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"
|
||||||
|
Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON represents any valid JSON value.
|
// JSON represents any valid JSON value.
|
||||||
|
@ -19,11 +19,13 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
|
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||||
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
@ -909,6 +911,40 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(schema.XValidations) > 0 {
|
||||||
|
for i, rule := range schema.XValidations {
|
||||||
|
trimmedRule := strings.TrimSpace(rule.Rule)
|
||||||
|
trimmedMsg := strings.TrimSpace(rule.Message)
|
||||||
|
if len(trimmedRule) == 0 {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), "rule is not specified"))
|
||||||
|
} else if len(rule.Message) > 0 && len(trimmedMsg) == 0 {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("message"), rule.Message, "message must be non-empty if specified"))
|
||||||
|
} else if hasNewlines(trimmedMsg) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("message"), rule.Message, "message must not contain line breaks"))
|
||||||
|
} else if hasNewlines(trimmedRule) && len(trimmedMsg) == 0 {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("message"), "message must be specified if rule contains line breaks"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
structural, err := structuralschema.NewStructural(schema)
|
||||||
|
if err == nil {
|
||||||
|
compResults, err := cel.Compile(structural, isRoot)
|
||||||
|
if err != nil {
|
||||||
|
allErrs = append(allErrs, field.InternalError(fldPath.Child("x-kubernetes-validations"), err))
|
||||||
|
} else {
|
||||||
|
for i, cr := range compResults {
|
||||||
|
if cr.Error != nil {
|
||||||
|
if cr.Error.Type == cel.ErrorTypeRequired {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), cr.Error.Detail))
|
||||||
|
} else {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i], cr.Error.Detail))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if opts.requireMapListKeysMapSetValidation {
|
if opts.requireMapListKeysMapSetValidation {
|
||||||
allErrs = append(allErrs, validateMapListKeysMapSet(schema, fldPath)...)
|
allErrs = append(allErrs, validateMapListKeysMapSet(schema, fldPath)...)
|
||||||
}
|
}
|
||||||
@ -916,6 +952,11 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var newlineMatcher = regexp.MustCompile(`[\n\r]+`) // valid newline chars in CEL grammar
|
||||||
|
func hasNewlines(s string) bool {
|
||||||
|
return newlineMatcher.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
func validateMapListKeysMapSet(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
|
func validateMapListKeysMapSet(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
@ -1112,7 +1153,7 @@ func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
|
|||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example", "XPreserveUnknownFields"}
|
var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example", "XPreserveUnknownFields", "XValidations"}
|
||||||
|
|
||||||
func allowedAtRootSchema(field string) bool {
|
func allowedAtRootSchema(field string) bool {
|
||||||
for _, v := range allowedFieldsAtRootSchema {
|
for _, v := range allowedFieldsAtRootSchema {
|
||||||
@ -1144,16 +1185,16 @@ func allVersionsSpecifyOpenAPISchema(spec *apiextensions.CustomResourceDefinitio
|
|||||||
}
|
}
|
||||||
|
|
||||||
func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
return hasSchemaWith(spec, schemaHasDefaults)
|
return HasSchemaWith(spec, schemaHasDefaults)
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
|
func schemaHasDefaults(s *apiextensions.JSONSchemaProps) bool {
|
||||||
return schemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
return SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
||||||
return s.Default != nil
|
return s.Default != nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasSchemaWith(spec *apiextensions.CustomResourceDefinitionSpec, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
|
func HasSchemaWith(spec *apiextensions.CustomResourceDefinitionSpec, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
|
||||||
if spec.Validation != nil && spec.Validation.OpenAPIV3Schema != nil && pred(spec.Validation.OpenAPIV3Schema) {
|
if spec.Validation != nil && spec.Validation.OpenAPIV3Schema != nil && pred(spec.Validation.OpenAPIV3Schema) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -1165,7 +1206,7 @@ func hasSchemaWith(spec *apiextensions.CustomResourceDefinitionSpec, pred func(s
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaHas(s *apiextensions.JSONSchemaProps, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
|
func SchemaHas(s *apiextensions.JSONSchemaProps, pred func(s *apiextensions.JSONSchemaProps) bool) bool {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1175,60 +1216,60 @@ func schemaHas(s *apiextensions.JSONSchemaProps, pred func(s *apiextensions.JSON
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.Items != nil {
|
if s.Items != nil {
|
||||||
if s.Items != nil && schemaHas(s.Items.Schema, pred) {
|
if s.Items != nil && SchemaHas(s.Items.Schema, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, s := range s.Items.JSONSchemas {
|
for _, s := range s.Items.JSONSchemas {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range s.AllOf {
|
for _, s := range s.AllOf {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range s.AnyOf {
|
for _, s := range s.AnyOf {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range s.OneOf {
|
for _, s := range s.OneOf {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if schemaHas(s.Not, pred) {
|
if SchemaHas(s.Not, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, s := range s.Properties {
|
for _, s := range s.Properties {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.AdditionalProperties != nil {
|
if s.AdditionalProperties != nil {
|
||||||
if schemaHas(s.AdditionalProperties.Schema, pred) {
|
if SchemaHas(s.AdditionalProperties.Schema, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range s.PatternProperties {
|
for _, s := range s.PatternProperties {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.AdditionalItems != nil {
|
if s.AdditionalItems != nil {
|
||||||
if schemaHas(s.AdditionalItems.Schema, pred) {
|
if SchemaHas(s.AdditionalItems.Schema, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range s.Definitions {
|
for _, s := range s.Definitions {
|
||||||
if schemaHas(&s, pred) {
|
if SchemaHas(&s, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, d := range s.Dependencies {
|
for _, d := range s.Dependencies {
|
||||||
if schemaHas(d.Schema, pred) {
|
if SchemaHas(d.Schema, pred) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1249,8 +1290,8 @@ func specHasKubernetesExtensions(spec *apiextensions.CustomResourceDefinitionSpe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func schemaHasKubernetesExtensions(s *apiextensions.JSONSchemaProps) bool {
|
func schemaHasKubernetesExtensions(s *apiextensions.JSONSchemaProps) bool {
|
||||||
return schemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
return SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
||||||
return s.XEmbeddedResource || s.XPreserveUnknownFields != nil || s.XIntOrString || len(s.XListMapKeys) > 0 || s.XListType != nil
|
return s.XEmbeddedResource || s.XPreserveUnknownFields != nil || s.XIntOrString || len(s.XListMapKeys) > 0 || s.XListType != nil || len(s.XValidations) > 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1322,12 +1363,12 @@ func schemaHasUnprunedDefaults(schema *apiextensions.JSONSchemaProps) (bool, err
|
|||||||
|
|
||||||
// requireAtomicSetType returns true if the old CRD spec as at least one x-kubernetes-list-type=set with non-atomic items type.
|
// requireAtomicSetType returns true if the old CRD spec as at least one x-kubernetes-list-type=set with non-atomic items type.
|
||||||
func requireAtomicSetType(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
|
func requireAtomicSetType(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
return !hasSchemaWith(oldCRDSpec, hasNonAtomicSetType)
|
return !HasSchemaWith(oldCRDSpec, hasNonAtomicSetType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasNonAtomicSetType recurses over the schema and returns whether any list of type "set" as non-atomic item types.
|
// hasNonAtomicSetType recurses over the schema and returns whether any list of type "set" as non-atomic item types.
|
||||||
func hasNonAtomicSetType(schema *apiextensions.JSONSchemaProps) bool {
|
func hasNonAtomicSetType(schema *apiextensions.JSONSchemaProps) bool {
|
||||||
return schemaHas(schema, func(schema *apiextensions.JSONSchemaProps) bool {
|
return SchemaHas(schema, func(schema *apiextensions.JSONSchemaProps) bool {
|
||||||
if schema.XListType != nil && *schema.XListType == "set" && schema.Items != nil && schema.Items.Schema != nil { // we don't support schema.Items.JSONSchemas
|
if schema.XListType != nil && *schema.XListType == "set" && schema.Items != nil && schema.Items.Schema != nil { // we don't support schema.Items.JSONSchemas
|
||||||
is := schema.Items.Schema
|
is := schema.Items.Schema
|
||||||
switch is.Type {
|
switch is.Type {
|
||||||
@ -1344,11 +1385,11 @@ func hasNonAtomicSetType(schema *apiextensions.JSONSchemaProps) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func requireMapListKeysMapSetValidation(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
|
func requireMapListKeysMapSetValidation(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
return !hasSchemaWith(oldCRDSpec, hasInvalidMapListKeysMapSet)
|
return !HasSchemaWith(oldCRDSpec, hasInvalidMapListKeysMapSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasInvalidMapListKeysMapSet(schema *apiextensions.JSONSchemaProps) bool {
|
func hasInvalidMapListKeysMapSet(schema *apiextensions.JSONSchemaProps) bool {
|
||||||
return schemaHas(schema, func(schema *apiextensions.JSONSchemaProps) bool {
|
return SchemaHas(schema, func(schema *apiextensions.JSONSchemaProps) bool {
|
||||||
return len(validateMapListKeysMapSet(schema, field.NewPath(""))) > 0
|
return len(validateMapListKeysMapSet(schema, field.NewPath(""))) > 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1426,7 +1467,7 @@ func specHasInvalidTypes(spec *apiextensions.CustomResourceDefinitionSpec) bool
|
|||||||
|
|
||||||
// SchemaHasInvalidTypes returns true if it contains invalid offending openapi-v3 specification.
|
// SchemaHasInvalidTypes returns true if it contains invalid offending openapi-v3 specification.
|
||||||
func SchemaHasInvalidTypes(s *apiextensions.JSONSchemaProps) bool {
|
func SchemaHasInvalidTypes(s *apiextensions.JSONSchemaProps) bool {
|
||||||
return schemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
return SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
||||||
return len(s.Type) > 0 && !openapiV3Types.Has(s.Type)
|
return len(s.Type) > 0 && !openapiV3Types.Has(s.Type)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
"github.com/google/cel-go/checker/decls"
|
||||||
|
"github.com/google/cel-go/ext"
|
||||||
|
|
||||||
|
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
celmodel "k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScopedVarName is the variable name assigned to the locally scoped data element of a CEL valid.
|
||||||
|
const ScopedVarName = "self"
|
||||||
|
|
||||||
|
// CompilationResult represents the cel compilation result for one rule
|
||||||
|
type CompilationResult struct {
|
||||||
|
Program cel.Program
|
||||||
|
Error *Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile compiles all the XValidations rules (without recursing into the schema) and returns a slice containing a
|
||||||
|
// CompilationResult for each ValidationRule, or an error.
|
||||||
|
// Each CompilationResult may contain:
|
||||||
|
/// - non-nil Program, nil Error: The program was compiled successfully
|
||||||
|
// - nil Program, non-nil Error: Compilation resulted in an error
|
||||||
|
// - nil Program, nil Error: The provided rule was empty so compilation was not attempted
|
||||||
|
func Compile(s *schema.Structural, isResourceRoot bool) ([]CompilationResult, error) {
|
||||||
|
if len(s.Extensions.XValidations) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
celRules := s.Extensions.XValidations
|
||||||
|
|
||||||
|
var propDecls []*expr.Decl
|
||||||
|
var root *celmodel.DeclType
|
||||||
|
var ok bool
|
||||||
|
env, err := cel.NewEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reg := celmodel.NewRegistry(env)
|
||||||
|
scopedTypeName := generateUniqueSelfTypeName()
|
||||||
|
rt, err := celmodel.NewRuleTypes(scopedTypeName, s, isResourceRoot, reg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if rt == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
opts, err := rt.EnvOptions(env.TypeProvider())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, ok = rt.FindDeclType(scopedTypeName)
|
||||||
|
if !ok {
|
||||||
|
rootDecl := celmodel.SchemaDeclType(s, isResourceRoot)
|
||||||
|
if rootDecl == nil {
|
||||||
|
return nil, fmt.Errorf("rule declared on schema that does not support validation rules type: '%s' x-kubernetes-preserve-unknown-fields: '%t'", s.Type, s.XPreserveUnknownFields)
|
||||||
|
}
|
||||||
|
root = rootDecl.MaybeAssignTypeName(scopedTypeName)
|
||||||
|
}
|
||||||
|
propDecls = append(propDecls, decls.NewVar(ScopedVarName, root.ExprType()))
|
||||||
|
opts = append(opts, cel.Declarations(propDecls...))
|
||||||
|
opts = append(opts, ext.Strings())
|
||||||
|
env, err = env.Extend(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// compResults is the return value which saves a list of compilation results in the same order as x-kubernetes-validations rules.
|
||||||
|
compResults := make([]CompilationResult, len(celRules))
|
||||||
|
for i, rule := range celRules {
|
||||||
|
var compilationResult CompilationResult
|
||||||
|
if len(strings.TrimSpace(rule.Rule)) == 0 {
|
||||||
|
// include a compilation result, but leave both program and error nil per documented return semantics of this
|
||||||
|
// function
|
||||||
|
} else {
|
||||||
|
ast, issues := env.Compile(rule.Rule)
|
||||||
|
if issues != nil {
|
||||||
|
compilationResult.Error = &Error{ErrorTypeInvalid, "compilation failed: " + issues.String()}
|
||||||
|
} else if !proto.Equal(ast.ResultType(), decls.Bool) {
|
||||||
|
compilationResult.Error = &Error{ErrorTypeInvalid, "cel expression must evaluate to a bool"}
|
||||||
|
} else {
|
||||||
|
prog, err := env.Program(ast)
|
||||||
|
if err != nil {
|
||||||
|
compilationResult.Error = &Error{ErrorTypeInvalid, "program instantiation failed: " + err.Error()}
|
||||||
|
} else {
|
||||||
|
compilationResult.Program = prog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compResults[i] = compilationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
return compResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateUniqueSelfTypeName creates a placeholder type name to use in a CEL programs for cases
|
||||||
|
// where we do not wish to expose a stable type name to CEL validator rule authors. For this to effectively prevent
|
||||||
|
// developers from depending on the generated name (i.e. using it in CEL programs), it must be changed each time a
|
||||||
|
// CRD is created or updated.
|
||||||
|
func generateUniqueSelfTypeName() string {
|
||||||
|
return fmt.Sprintf("selfType%d", time.Now().Nanosecond())
|
||||||
|
}
|
@ -0,0 +1,491 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type validationMatch struct {
|
||||||
|
errorType ErrorType
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidError(contains string) validationMatch {
|
||||||
|
return validationMatch{errorType: ErrorTypeInvalid, contains: contains}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v validationMatch) matches(err *Error) bool {
|
||||||
|
return err.Type == v.errorType && strings.Contains(err.Error(), v.contains)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCelCompilation(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
input schema.Structural
|
||||||
|
expectedErrors []validationMatch
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid object",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"minReplicas": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"maxReplicas": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self.minReplicas < self.maxReplicas",
|
||||||
|
Message: "minReplicas should be smaller than maxReplicas",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid for string",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self.startsWith('s')",
|
||||||
|
Message: "scoped field should start with 's'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid for byte",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
ValueValidation: &schema.ValueValidation{
|
||||||
|
Format: "byte",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "string(self).endsWith('s')",
|
||||||
|
Message: "scoped field should end with 's'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid for boolean",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self == true",
|
||||||
|
Message: "scoped field should be true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid for integer",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self > 0",
|
||||||
|
Message: "scoped field should be greater than 0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid for number",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self > 1.0",
|
||||||
|
Message: "scoped field should be greater than 1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid nested object of object",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"nestedObj": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"val": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
ValueValidation: &schema.ValueValidation{
|
||||||
|
Format: "int64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self.nestedObj.val == 10",
|
||||||
|
Message: "val should be equal to 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid nested object of array",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"nestedObj": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self.nestedObj[0]) == 10",
|
||||||
|
Message: "size of first element in nestedObj should be equal to 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid nested array of array",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self[0][0]) == 10",
|
||||||
|
Message: "size of items under items of scoped field should be equal to 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid nested array of object",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"nestedObj": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"val": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
ValueValidation: &schema.ValueValidation{
|
||||||
|
Format: "int64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "self[0].nestedObj.val == 10",
|
||||||
|
Message: "val under nestedObj under properties under items should be equal to 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid map",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
AdditionalProperties: &schema.StructuralOrBool{
|
||||||
|
Bool: true,
|
||||||
|
Structural: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "boolean",
|
||||||
|
Nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid checking for number",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) == 10",
|
||||||
|
Message: "size of scoped field should be equal to 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalidError("compilation failed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "compilation failure",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) == 10",
|
||||||
|
Message: "size of scoped field should be equal to 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalidError("compilation failed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid for escaping",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"namespace": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"if": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self.__namespace__[0]) == 10",
|
||||||
|
Message: "size of first element in nestedObj should be equal to 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule: "self.__if__ == 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule: "self.self == 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid for escaping",
|
||||||
|
input: schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
Properties: map[string]schema.Structural{
|
||||||
|
"namespace": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "array",
|
||||||
|
},
|
||||||
|
Items: &schema.Structural{
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"if": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
Generic: schema.Generic{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extensions: schema.Extensions{
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self.namespace[0]) == 10",
|
||||||
|
Message: "size of first element in nestedObj should be equal to 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule: "self.if == 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rule: "self == 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErrors: []validationMatch{
|
||||||
|
invalidError("undefined field 'namespace'"),
|
||||||
|
invalidError("undefined field 'if'"),
|
||||||
|
invalidError("found no matching overload"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range cases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
compilationResults, err := Compile(&tt.input, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seenErrs := make([]bool, len(compilationResults))
|
||||||
|
|
||||||
|
for _, expectedError := range tt.expectedErrors {
|
||||||
|
found := false
|
||||||
|
for i, result := range compilationResults {
|
||||||
|
if expectedError.matches(result.Error) && !seenErrs[i] {
|
||||||
|
found = true
|
||||||
|
seenErrs[i] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected error: %v", expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, seen := range seenErrs {
|
||||||
|
if !seen && compilationResults[i].Error != nil {
|
||||||
|
t.Errorf("unexpected error: %v", compilationResults[i].Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cel
|
||||||
|
|
||||||
|
// Error is an implementation of the 'error' interface, which represents a
|
||||||
|
// XValidation error.
|
||||||
|
type Error struct {
|
||||||
|
Type ErrorType
|
||||||
|
Detail string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = &Error{}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (v *Error) Error() string {
|
||||||
|
return v.Detail
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorType is a machine readable value providing more detail about why
|
||||||
|
// a XValidation is invalid.
|
||||||
|
type ErrorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrorTypeRequired is used to report withNullable values that are not
|
||||||
|
// provided (e.g. empty strings, null values, or empty arrays). See
|
||||||
|
// Required().
|
||||||
|
ErrorTypeRequired ErrorType = "RuleRequired"
|
||||||
|
// ErrorTypeInvalid is used to report malformed values
|
||||||
|
ErrorTypeInvalid ErrorType = "RuleInvalid"
|
||||||
|
// ErrorTypeInternal is used to report other errors that are not related
|
||||||
|
// to user input. See InternalError().
|
||||||
|
ErrorTypeInternal ErrorType = "InternalError"
|
||||||
|
)
|
@ -18,7 +18,6 @@ package schema
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -247,6 +246,7 @@ func newExtensions(s *apiextensions.JSONSchemaProps) (*Extensions, error) {
|
|||||||
XListMapKeys: s.XListMapKeys,
|
XListMapKeys: s.XListMapKeys,
|
||||||
XListType: s.XListType,
|
XListType: s.XListType,
|
||||||
XMapType: s.XMapType,
|
XMapType: s.XMapType,
|
||||||
|
XValidations: s.XValidations,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.XPreserveUnknownFields != nil {
|
if s.XPreserveUnknownFields != nil {
|
||||||
|
@ -87,6 +87,9 @@ func (x *Extensions) toKubeOpenAPI(ret *spec.Schema) {
|
|||||||
if x.XMapType != nil {
|
if x.XMapType != nil {
|
||||||
ret.VendorExtensible.AddExtension("x-kubernetes-map-type", *x.XMapType)
|
ret.VendorExtensible.AddExtension("x-kubernetes-map-type", *x.XMapType)
|
||||||
}
|
}
|
||||||
|
if len(x.XValidations) > 0 {
|
||||||
|
ret.VendorExtensible.AddExtension("x-kubernetes-validations", x.XValidations)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *ValueValidation) toKubeOpenAPI(ret *spec.Schema) {
|
func (v *ValueValidation) toKubeOpenAPI(ret *spec.Schema) {
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -127,6 +128,9 @@ type Extensions struct {
|
|||||||
// Atomic maps will be entirely replaced when updated.
|
// Atomic maps will be entirely replaced when updated.
|
||||||
// +optional
|
// +optional
|
||||||
XMapType *string
|
XMapType *string
|
||||||
|
|
||||||
|
// x-kubernetes-validations describes a list of validation rules for expression validation.
|
||||||
|
XValidations apiextensions.ValidationRules
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen=true
|
// +k8s:deepcopy-gen=true
|
||||||
|
@ -324,6 +324,9 @@ func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllO
|
|||||||
if v.ForbiddenExtensions.XMapType != nil {
|
if v.ForbiddenExtensions.XMapType != nil {
|
||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-map-type"), "must be undefined to be structural"))
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-map-type"), "must be undefined to be structural"))
|
||||||
}
|
}
|
||||||
|
if len(v.ForbiddenExtensions.XValidations) > 0 {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-validations"), "must be empty to be structural"))
|
||||||
|
}
|
||||||
|
|
||||||
// forbid reasoning about metadata because it can lead to metadata restriction we don't want
|
// forbid reasoning about metadata because it can lead to metadata restriction we don't want
|
||||||
if _, found := v.Properties["metadata"]; found {
|
if _, found := v.Properties["metadata"]; found {
|
||||||
|
@ -18,19 +18,9 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/cel-go/cel"
|
|
||||||
"github.com/google/cel-go/checker/decls"
|
|
||||||
"github.com/google/cel-go/common/types"
|
|
||||||
"github.com/google/cel-go/ext"
|
|
||||||
|
|
||||||
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
|
||||||
"k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model"
|
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
openapierrors "k8s.io/kube-openapi/pkg/validation/errors"
|
openapierrors "k8s.io/kube-openapi/pkg/validation/errors"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
@ -264,6 +254,9 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
|
|||||||
if in.XMapType != nil {
|
if in.XMapType != nil {
|
||||||
out.VendorExtensible.AddExtension("x-kubernetes-map-type", *in.XMapType)
|
out.VendorExtensible.AddExtension("x-kubernetes-map-type", *in.XMapType)
|
||||||
}
|
}
|
||||||
|
if len(in.XValidations) != 0 {
|
||||||
|
out.VendorExtensible.AddExtension("x-kubernetes-validations", in.XValidations)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,45 +332,3 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompileAndValidate provides a sanity check of the CEL validation functionality until we wire in the
|
|
||||||
// full functionality.
|
|
||||||
func CompileAndValidate(s *schema.Structural, bindings map[string]interface{}, rule string) (bool, error) {
|
|
||||||
env, err := cel.NewEnv()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
reg := model.NewRegistry(env)
|
|
||||||
rt, err := model.NewRuleTypes("testType", s, reg)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
opts, err := rt.EnvOptions(env.TypeProvider())
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
root, ok := rt.FindDeclType("testType")
|
|
||||||
if !ok {
|
|
||||||
root = model.SchemaDeclType(s).MaybeAssignTypeName("testType")
|
|
||||||
}
|
|
||||||
propDecls := []*expr.Decl{decls.NewVar("self", root.ExprType())}
|
|
||||||
opts = append(opts, cel.Declarations(propDecls...))
|
|
||||||
opts = append(opts, ext.Strings())
|
|
||||||
env, err = env.Extend(opts...)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
ast, issues := env.Compile(rule)
|
|
||||||
if issues != nil {
|
|
||||||
return false, fmt.Errorf("issues: %v", issues)
|
|
||||||
}
|
|
||||||
prog, err := env.Program(ast)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
evalResult, _, err := prog.Eval(bindings)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return evalResult == types.True, nil
|
|
||||||
}
|
|
||||||
|
@ -23,7 +23,6 @@ import (
|
|||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -485,32 +484,3 @@ func TestItemsProperty(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCompileAndValidate is a placeholder test of CEL validation that will be removed when the actual functionality
|
|
||||||
// is wired in.
|
|
||||||
func TestCompileAndValidate(t *testing.T) {
|
|
||||||
s := &schema.Structural{
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Type: "object",
|
|
||||||
},
|
|
||||||
Properties: map[string]schema.Structural{
|
|
||||||
"name": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Type: "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bindings := map[string]interface{}{
|
|
||||||
"self": map[string]interface{}{
|
|
||||||
"name": "kube",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
result, err := CompileAndValidate(s, bindings, "self.name == 'kube'")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !result {
|
|
||||||
t.Error("Expected expression to evaluate to true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -28,9 +28,11 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
"k8s.io/apiserver/pkg/storage"
|
"k8s.io/apiserver/pkg/storage"
|
||||||
"k8s.io/apiserver/pkg/storage/names"
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -77,6 +79,8 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropDisabledFields(crd, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||||
@ -105,6 +109,8 @@ func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropDisabledFields(newCRD, oldCRD)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates a new CustomResourceDefinition.
|
// Validate validates a new CustomResourceDefinition.
|
||||||
@ -222,3 +228,54 @@ func MatchCustomResourceDefinition(label labels.Selector, field fields.Selector)
|
|||||||
func CustomResourceDefinitionToSelectableFields(obj *apiextensions.CustomResourceDefinition) fields.Set {
|
func CustomResourceDefinitionToSelectableFields(obj *apiextensions.CustomResourceDefinition) fields.Set {
|
||||||
return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true)
|
return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dropDisabledFields drops disabled fields that are not used if their associated feature gates
|
||||||
|
// are not enabled.
|
||||||
|
func dropDisabledFields(newCRD *apiextensions.CustomResourceDefinition, oldCRD *apiextensions.CustomResourceDefinition) {
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) && (oldCRD == nil || (oldCRD != nil && !specHasXValidations(&oldCRD.Spec))) {
|
||||||
|
if newCRD.Spec.Validation != nil {
|
||||||
|
dropXValidationsField(newCRD.Spec.Validation.OpenAPIV3Schema)
|
||||||
|
}
|
||||||
|
for _, v := range newCRD.Spec.Versions {
|
||||||
|
if v.Schema != nil {
|
||||||
|
dropXValidationsField(v.Schema.OpenAPIV3Schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropXValidationsField drops field XValidations from CRD schema
|
||||||
|
func dropXValidationsField(schema *apiextensions.JSONSchemaProps) {
|
||||||
|
if schema == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schema.XValidations = nil
|
||||||
|
if schema.AdditionalProperties != nil {
|
||||||
|
dropXValidationsField(schema.AdditionalProperties.Schema)
|
||||||
|
}
|
||||||
|
for def, jsonSchema := range schema.Properties {
|
||||||
|
dropXValidationsField(&jsonSchema)
|
||||||
|
schema.Properties[def] = jsonSchema
|
||||||
|
}
|
||||||
|
if schema.Items != nil {
|
||||||
|
dropXValidationsField(schema.Items.Schema)
|
||||||
|
for i, jsonSchema := range schema.Items.JSONSchemas {
|
||||||
|
dropXValidationsField(&jsonSchema)
|
||||||
|
schema.Items.JSONSchemas[i] = jsonSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for def, jsonSchemaPropsOrStringArray := range schema.Dependencies {
|
||||||
|
dropXValidationsField(jsonSchemaPropsOrStringArray.Schema)
|
||||||
|
schema.Dependencies[def] = jsonSchemaPropsOrStringArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func specHasXValidations(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||||
|
return validation.HasSchemaWith(spec, schemaHasXValidations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func schemaHasXValidations(s *apiextensions.JSONSchemaProps) bool {
|
||||||
|
return validation.SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
||||||
|
return s.XValidations != nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -19,11 +19,15 @@ package customresourcedefinition
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -187,3 +191,483 @@ func TestValidateAPIApproval(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDropDisabledFields tests if the drop functionality is working fine or not with feature gate switch
|
||||||
|
func TestDropDisabledFields(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
enableXValidations bool
|
||||||
|
crd *apiextensions.CustomResourceDefinition
|
||||||
|
oldCRD *apiextensions.CustomResourceDefinition
|
||||||
|
expectedCRD *apiextensions.CustomResourceDefinition
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "For creation, FG disabled, no XValidations, no field drop",
|
||||||
|
enableXValidations: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For creation, FG disabled, empty XValidations, no field drop",
|
||||||
|
enableXValidations: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For creation, FG disabled, set XValidations, drop XValidations",
|
||||||
|
enableXValidations: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For creation, FG enabled, set XValidations, update with XValidations",
|
||||||
|
enableXValidations: true,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: nil,
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For update, FG disabled, oldCRD XValidation in use, don't drop XValidations",
|
||||||
|
enableXValidations: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: apiextensions.JSONSchemaDependencies{
|
||||||
|
"test": apiextensions.JSONSchemaPropsOrStringArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "size of scoped field should be greater than 0.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"subRule": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "isTest == true",
|
||||||
|
Message: "isTest should be true.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"isTest": {
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For update, FG disabled, oldCRD has no XValidations, drop XValidations",
|
||||||
|
enableXValidations: false,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For update, FG enabled, oldCRD has XValidations, updated to newCRD",
|
||||||
|
enableXValidations: true,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "old data",
|
||||||
|
Message: "old data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "For update, FG enabled, oldCRD has no XValidations, updated to newCRD",
|
||||||
|
enableXValidations: true,
|
||||||
|
crd: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oldCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCRD: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foos.sigs.k8s.io", Annotations: map[string]string{v1beta1.KubeAPIApprovedAnnotation: "valid"}, ResourceVersion: "1"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Validation: &apiextensions.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensions.ValidationRules{
|
||||||
|
{
|
||||||
|
Rule: "size(self) > 0",
|
||||||
|
Message: "openAPIV3Schema should contain more than 0 element.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceValidationExpressions, tc.enableXValidations)()
|
||||||
|
old := tc.oldCRD.DeepCopy()
|
||||||
|
|
||||||
|
dropDisabledFields(tc.crd, tc.oldCRD)
|
||||||
|
|
||||||
|
// old crd should never be changed
|
||||||
|
if diff := cmp.Diff(tc.oldCRD, old); diff != "" {
|
||||||
|
t.Fatalf("old crd changed from %v to %v", tc.oldCRD, old)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tc.expectedCRD, tc.crd); diff != "" {
|
||||||
|
t.Fatalf("unexpected crd: %v", tc.crd)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user