mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +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,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,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,Schema
|
||||
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,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,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,Schema
|
||||
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.
|
||||
// +optional
|
||||
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.
|
||||
|
@ -161,6 +161,80 @@ type JSONSchemaProps struct {
|
||||
// Atomic maps will be entirely replaced when updated.
|
||||
// +optional
|
||||
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.
|
||||
|
@ -161,6 +161,80 @@ type JSONSchemaProps struct {
|
||||
// Atomic maps will be entirely replaced when updated.
|
||||
// +optional
|
||||
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.
|
||||
|
@ -19,11 +19,13 @@ package validation
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
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 {
|
||||
allErrs = append(allErrs, validateMapListKeysMapSet(schema, fldPath)...)
|
||||
}
|
||||
@ -916,6 +952,11 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
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 {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
@ -1112,7 +1153,7 @@ func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
|
||||
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 {
|
||||
for _, v := range allowedFieldsAtRootSchema {
|
||||
@ -1144,16 +1185,16 @@ func allVersionsSpecifyOpenAPISchema(spec *apiextensions.CustomResourceDefinitio
|
||||
}
|
||||
|
||||
func specHasDefaults(spec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||
return hasSchemaWith(spec, schemaHasDefaults)
|
||||
return HasSchemaWith(spec, schemaHasDefaults)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
return true
|
||||
}
|
||||
@ -1165,7 +1206,7 @@ func hasSchemaWith(spec *apiextensions.CustomResourceDefinitionSpec, pred func(s
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
@ -1175,60 +1216,60 @@ func schemaHas(s *apiextensions.JSONSchemaProps, pred func(s *apiextensions.JSON
|
||||
}
|
||||
|
||||
if s.Items != nil {
|
||||
if s.Items != nil && schemaHas(s.Items.Schema, pred) {
|
||||
if s.Items != nil && SchemaHas(s.Items.Schema, pred) {
|
||||
return true
|
||||
}
|
||||
for _, s := range s.Items.JSONSchemas {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range s.AllOf {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range s.AnyOf {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range s.OneOf {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if schemaHas(s.Not, pred) {
|
||||
if SchemaHas(s.Not, pred) {
|
||||
return true
|
||||
}
|
||||
for _, s := range s.Properties {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if s.AdditionalProperties != nil {
|
||||
if schemaHas(s.AdditionalProperties.Schema, pred) {
|
||||
if SchemaHas(s.AdditionalProperties.Schema, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range s.PatternProperties {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if s.AdditionalItems != nil {
|
||||
if schemaHas(s.AdditionalItems.Schema, pred) {
|
||||
if SchemaHas(s.AdditionalItems.Schema, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range s.Definitions {
|
||||
if schemaHas(&s, pred) {
|
||||
if SchemaHas(&s, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, d := range s.Dependencies {
|
||||
if schemaHas(d.Schema, pred) {
|
||||
if SchemaHas(d.Schema, pred) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -1249,8 +1290,8 @@ func specHasKubernetesExtensions(spec *apiextensions.CustomResourceDefinitionSpe
|
||||
}
|
||||
|
||||
func schemaHasKubernetesExtensions(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 SchemaHas(s, func(s *apiextensions.JSONSchemaProps) bool {
|
||||
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.
|
||||
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.
|
||||
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
|
||||
is := schema.Items.Schema
|
||||
switch is.Type {
|
||||
@ -1344,11 +1385,11 @@ func hasNonAtomicSetType(schema *apiextensions.JSONSchemaProps) bool {
|
||||
}
|
||||
|
||||
func requireMapListKeysMapSetValidation(oldCRDSpec *apiextensions.CustomResourceDefinitionSpec) bool {
|
||||
return !hasSchemaWith(oldCRDSpec, hasInvalidMapListKeysMapSet)
|
||||
return !HasSchemaWith(oldCRDSpec, hasInvalidMapListKeysMapSet)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
@ -1426,7 +1467,7 @@ func specHasInvalidTypes(spec *apiextensions.CustomResourceDefinitionSpec) bool
|
||||
|
||||
// SchemaHasInvalidTypes returns true if it contains invalid offending openapi-v3 specification.
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
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 (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
)
|
||||
|
||||
@ -247,6 +246,7 @@ func newExtensions(s *apiextensions.JSONSchemaProps) (*Extensions, error) {
|
||||
XListMapKeys: s.XListMapKeys,
|
||||
XListType: s.XListType,
|
||||
XMapType: s.XMapType,
|
||||
XValidations: s.XValidations,
|
||||
}
|
||||
|
||||
if s.XPreserveUnknownFields != nil {
|
||||
|
@ -87,6 +87,9 @@ func (x *Extensions) toKubeOpenAPI(ret *spec.Schema) {
|
||||
if x.XMapType != nil {
|
||||
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) {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package schema
|
||||
|
||||
import (
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@ -127,6 +128,9 @@ type Extensions struct {
|
||||
// Atomic maps will be entirely replaced when updated.
|
||||
// +optional
|
||||
XMapType *string
|
||||
|
||||
// x-kubernetes-validations describes a list of validation rules for expression validation.
|
||||
XValidations apiextensions.ValidationRules
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
@ -324,6 +324,9 @@ func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllO
|
||||
if v.ForbiddenExtensions.XMapType != nil {
|
||||
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
|
||||
if _, found := v.Properties["metadata"]; found {
|
||||
|
@ -18,19 +18,9 @@ package validation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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/apiserver/schema"
|
||||
"k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
openapierrors "k8s.io/kube-openapi/pkg/validation/errors"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
@ -264,6 +254,9 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
|
||||
if in.XMapType != nil {
|
||||
out.VendorExtensible.AddExtension("x-kubernetes-map-type", *in.XMapType)
|
||||
}
|
||||
if len(in.XValidations) != 0 {
|
||||
out.VendorExtensible.AddExtension("x-kubernetes-validations", in.XValidations)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -339,45 +332,3 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri
|
||||
}
|
||||
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"
|
||||
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"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/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
)
|
||||
|
||||
@ -77,6 +79,8 @@ func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
dropDisabledFields(crd, nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
dropDisabledFields(newCRD, oldCRD)
|
||||
}
|
||||
|
||||
// Validate validates a new CustomResourceDefinition.
|
||||
@ -222,3 +228,54 @@ func MatchCustomResourceDefinition(label labels.Selector, field fields.Selector)
|
||||
func CustomResourceDefinitionToSelectableFields(obj *apiextensions.CustomResourceDefinition) fields.Set {
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -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