mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +00:00
Reuse structural schema and cel decls during CRD validation
This commit is contained in:
parent
42786afae0
commit
81bd325a72
@ -0,0 +1,335 @@
|
||||
/*
|
||||
Copyright 2022 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 validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// unbounded uses nil to represent an unbounded cardinality value.
|
||||
var unbounded *uint64 = nil
|
||||
|
||||
// CELSchemaContext keeps track of data used by x-kubernetes-validations rules for a specific schema node.
|
||||
type CELSchemaContext struct {
|
||||
// withinValidationRuleScope is true if the schema at the current level or above have x-kubernetes-validations rules. typeInfo
|
||||
// should only be populated for schema nodes where this is true.
|
||||
withinValidationRuleScope bool
|
||||
|
||||
// typeInfo is lazily loaded for schema nodes withinValidationRuleScope and may be
|
||||
// populated one of two possible ways:
|
||||
// 1. Using a typeInfoAccessor to access it from the parent's type info. This is a cheap operation and should be
|
||||
// used when a schema at a higher level already has type info.
|
||||
// 2. Using a converter to construct type info from the jsonSchema. This is an expensive operation.
|
||||
typeInfo *CELTypeInfo
|
||||
// typeInfoErr is any cached error resulting from an attempt to lazily load typeInfo.
|
||||
typeInfoErr error
|
||||
|
||||
// parent is the context of the parent schema node, or nil if this is the context for the root schema node.
|
||||
parent *CELSchemaContext
|
||||
// typeInfoAccessor provides a way to access the type info of this schema node from the parent CELSchemaContext.
|
||||
// nil if not extraction is possible, or the parent is nil.
|
||||
typeInfoAccessor typeInfoAccessor
|
||||
|
||||
// jsonSchema is the schema for this CELSchemaContext node. It must be non-nil.
|
||||
jsonSchema *apiextensions.JSONSchemaProps
|
||||
// converter converts a JSONSchemaProps to CELTypeInfo.
|
||||
// Tests that check how many conversions are performed during CRD validation wrap DefaultConverter
|
||||
// with a converter that counts how many conversion operations.
|
||||
converter converter
|
||||
|
||||
// MaxCardinality represents a limit to the number of data elements that can exist for the current
|
||||
// schema based on MaxProperties or MaxItems limits present on parent schemas, If all parent
|
||||
// map and array schemas have MaxProperties or MaxItems limits declared MaxCardinality is
|
||||
// an int pointer representing the product of these limits. If least one parent map or list schema
|
||||
// does not have a MaxProperties or MaxItems limits set, the MaxCardinality is nil, indicating
|
||||
// that the parent schemas offer no bound to the number of times a data element for the current
|
||||
// schema can exist.
|
||||
MaxCardinality *uint64
|
||||
// TotalCost accumulates the x-kubernetes-validators estimated rule cost total for an entire custom resource
|
||||
// definition. A single TotalCost is allocated for each CustomResourceDefinition and passed through the stack as the
|
||||
// CustomResourceDefinition's OpenAPIv3 schema is recursively validated.
|
||||
TotalCost *TotalCost
|
||||
}
|
||||
|
||||
// TypeInfo returns the CELTypeInfo for this CELSchemaContext node. Returns nil, nil if this CELSchemaContext is nil,
|
||||
// or if current level or above does not have x-kubernetes-validations rules. The returned type info is shared and
|
||||
// should not be modified by the caller.
|
||||
func (c *CELSchemaContext) TypeInfo() (*CELTypeInfo, error) {
|
||||
if c == nil || !c.withinValidationRuleScope {
|
||||
return nil, nil
|
||||
}
|
||||
if c.typeInfo != nil || c.typeInfoErr != nil {
|
||||
return c.typeInfo, c.typeInfoErr // return already computed result if available
|
||||
}
|
||||
|
||||
// If able to get the type info from the parent's type info, prefer this approach
|
||||
// since it is more efficient.
|
||||
if c.parent != nil {
|
||||
parentTypeInfo, parentErr := c.parent.TypeInfo()
|
||||
if parentErr != nil {
|
||||
c.typeInfoErr = parentErr
|
||||
return nil, parentErr
|
||||
}
|
||||
if parentTypeInfo != nil && c.typeInfoAccessor != nil {
|
||||
c.typeInfo = c.typeInfoAccessor.accessTypeInfo(parentTypeInfo)
|
||||
if c.typeInfo != nil {
|
||||
return c.typeInfo, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// If unable to get the type info from the parent, convert the jsonSchema to type info.
|
||||
// This is expensive for large schemas.
|
||||
c.typeInfo, c.typeInfoErr = c.converter(c.jsonSchema, c.parent == nil || c.jsonSchema.XEmbeddedResource)
|
||||
return c.typeInfo, c.typeInfoErr
|
||||
}
|
||||
|
||||
// CELTypeInfo represents all the typeInfo needed by CEL to compile x-kubernetes-validations rules for a schema node.
|
||||
type CELTypeInfo struct {
|
||||
// Schema is a structural schema for this CELSchemaContext node. It must be non-nil.
|
||||
Schema *structuralschema.Structural
|
||||
// DeclType is a CEL declaration representation of Schema of this CELSchemaContext node. It must be non-nil.
|
||||
DeclType *model.DeclType
|
||||
}
|
||||
|
||||
// converter converts from JSON schema to a structural schema and a CEL declType, or returns an error if the conversion
|
||||
// fails. This should be defaultConverter except in tests where it is useful to wrap it with a converter that tracks
|
||||
// how many conversions have been performed.
|
||||
type converter func(schema *apiextensions.JSONSchemaProps, isRoot bool) (*CELTypeInfo, error)
|
||||
|
||||
func defaultConverter(schema *apiextensions.JSONSchemaProps, isRoot bool) (*CELTypeInfo, error) {
|
||||
structural, err := structuralschema.NewStructural(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
declType := model.SchemaDeclType(structural, isRoot)
|
||||
if declType == nil {
|
||||
return nil, fmt.Errorf("unable to convert structural schema to CEL declarations")
|
||||
}
|
||||
return &CELTypeInfo{structural, declType}, nil
|
||||
}
|
||||
|
||||
// RootCELContext constructs CELSchemaContext for the given root schema.
|
||||
func RootCELContext(schema *apiextensions.JSONSchemaProps) *CELSchemaContext {
|
||||
rootCardinality := uint64(1)
|
||||
r := &CELSchemaContext{
|
||||
jsonSchema: schema,
|
||||
withinValidationRuleScope: len(schema.XValidations) > 0,
|
||||
MaxCardinality: &rootCardinality,
|
||||
TotalCost: &TotalCost{},
|
||||
converter: defaultConverter,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ChildPropertyContext returns nil, nil if this CELSchemaContext is nil, otherwise constructs and returns a
|
||||
// CELSchemaContext for propertyName.
|
||||
func (c *CELSchemaContext) ChildPropertyContext(propSchema *apiextensions.JSONSchemaProps, propertyName string) *CELSchemaContext {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.childContext(propSchema, propertyTypeInfoAccessor{propertyName: propertyName})
|
||||
}
|
||||
|
||||
// ChildAdditionalPropertiesContext returns nil, nil if this CELSchemaContext is nil, otherwise it constructs and returns
|
||||
// a CELSchemaContext for the properties of an object if this CELSchemaContext is an object.
|
||||
// schema must be non-nil and have a non-nil schema.AdditionalProperties.
|
||||
func (c *CELSchemaContext) ChildAdditionalPropertiesContext(propsSchema *apiextensions.JSONSchemaProps) *CELSchemaContext {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.childContext(propsSchema, additionalItemsTypeInfoAccessor{})
|
||||
}
|
||||
|
||||
// ChildItemsContext returns nil, nil if this CELSchemaContext is nil, otherwise it constructs and returns a CELSchemaContext
|
||||
// for the items of an array if this CELSchemaContext is an array.
|
||||
func (c *CELSchemaContext) ChildItemsContext(itemsSchema *apiextensions.JSONSchemaProps) *CELSchemaContext {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.childContext(itemsSchema, itemsTypeInfoAccessor{})
|
||||
}
|
||||
|
||||
// childContext returns nil, nil if this CELSchemaContext is nil, otherwise it constructs a new CELSchemaContext for the
|
||||
// given child schema of the current schema context.
|
||||
// accessor optionally provides a way to access CELTypeInfo of the child from the current schema context's CELTypeInfo.
|
||||
// childContext returns a CELSchemaContext where the MaxCardinality is multiplied by the
|
||||
// factor that the schema increases the cardinality of its children. If the CELSchemaContext's
|
||||
// MaxCardinality is unbounded (nil) or the factor that the schema increase the cardinality
|
||||
// is unbounded, the resulting CELSchemaContext's MaxCardinality is also unbounded.
|
||||
func (c *CELSchemaContext) childContext(child *apiextensions.JSONSchemaProps, accessor typeInfoAccessor) *CELSchemaContext {
|
||||
result := &CELSchemaContext{
|
||||
parent: c,
|
||||
typeInfoAccessor: accessor,
|
||||
withinValidationRuleScope: c.withinValidationRuleScope,
|
||||
TotalCost: c.TotalCost,
|
||||
MaxCardinality: unbounded,
|
||||
converter: c.converter,
|
||||
}
|
||||
if child != nil {
|
||||
result.jsonSchema = child
|
||||
if len(child.XValidations) > 0 {
|
||||
result.withinValidationRuleScope = true
|
||||
}
|
||||
}
|
||||
if c.jsonSchema == nil {
|
||||
// nil schemas can be passed since we call ChildSchemaContext
|
||||
// before ValidateCustomResourceDefinitionOpenAPISchema performs its nil check
|
||||
return result
|
||||
}
|
||||
if c.MaxCardinality == unbounded {
|
||||
return result
|
||||
}
|
||||
maxElements := extractMaxElements(c.jsonSchema)
|
||||
if maxElements == unbounded {
|
||||
return result
|
||||
}
|
||||
result.MaxCardinality = uint64ptr(multiplyWithOverflowGuard(*c.MaxCardinality, *maxElements))
|
||||
return result
|
||||
}
|
||||
|
||||
type typeInfoAccessor interface {
|
||||
// accessTypeInfo looks up type information for a child schema from a non-nil parentTypeInfo and returns it,
|
||||
// or returns nil if the child schema information is not accessible. For example, a nil
|
||||
// return value is expected when a property name is unescapable in CEL.
|
||||
// The caller MUST ensure the provided parentTypeInfo is non-nil.
|
||||
accessTypeInfo(parentTypeInfo *CELTypeInfo) *CELTypeInfo
|
||||
}
|
||||
|
||||
type propertyTypeInfoAccessor struct {
|
||||
// propertyName is the property name in the parent schema that this schema is declared at.
|
||||
propertyName string
|
||||
}
|
||||
|
||||
func (c propertyTypeInfoAccessor) accessTypeInfo(parentTypeInfo *CELTypeInfo) *CELTypeInfo {
|
||||
if parentTypeInfo.Schema.Properties != nil {
|
||||
propSchema := parentTypeInfo.Schema.Properties[c.propertyName]
|
||||
if escapedPropName, ok := model.Escape(c.propertyName); ok {
|
||||
if fieldDeclType, ok := parentTypeInfo.DeclType.Fields[escapedPropName]; ok {
|
||||
return &CELTypeInfo{Schema: &propSchema, DeclType: fieldDeclType.Type}
|
||||
} // else fields with unknown types are omitted from CEL validation entirely
|
||||
} // fields with unescapable names are expected to be absent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type itemsTypeInfoAccessor struct{}
|
||||
|
||||
func (c itemsTypeInfoAccessor) accessTypeInfo(parentTypeInfo *CELTypeInfo) *CELTypeInfo {
|
||||
if parentTypeInfo.Schema.Items != nil {
|
||||
itemsSchema := parentTypeInfo.Schema.Items
|
||||
itemsDeclType := parentTypeInfo.DeclType.ElemType
|
||||
return &CELTypeInfo{Schema: itemsSchema, DeclType: itemsDeclType}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type additionalItemsTypeInfoAccessor struct{}
|
||||
|
||||
func (c additionalItemsTypeInfoAccessor) accessTypeInfo(parentTypeInfo *CELTypeInfo) *CELTypeInfo {
|
||||
if parentTypeInfo.Schema.AdditionalProperties != nil {
|
||||
propsSchema := parentTypeInfo.Schema.AdditionalProperties.Structural
|
||||
valuesDeclType := parentTypeInfo.DeclType.ElemType
|
||||
return &CELTypeInfo{Schema: propsSchema, DeclType: valuesDeclType}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TotalCost tracks the total cost of evaluating all the x-kubernetes-validations rules of a CustomResourceDefinition.
|
||||
type TotalCost struct {
|
||||
// Total accumulates the x-kubernetes-validations estimated rule cost total.
|
||||
Total uint64
|
||||
// MostExpensive accumulates the top 4 most expensive rules contributing to the Total. Only rules
|
||||
// that accumulate at least 1% of total cost limit are included.
|
||||
MostExpensive []RuleCost
|
||||
}
|
||||
|
||||
// ObserveExpressionCost accumulates the cost of evaluating a -kubernetes-validations rule.
|
||||
func (c *TotalCost) ObserveExpressionCost(path *field.Path, cost uint64) {
|
||||
if math.MaxUint64-c.Total < cost {
|
||||
c.Total = math.MaxUint64
|
||||
} else {
|
||||
c.Total += cost
|
||||
}
|
||||
|
||||
if cost < StaticEstimatedCRDCostLimit/100 { // ignore rules that contribute < 1% of total cost limit
|
||||
return
|
||||
}
|
||||
c.MostExpensive = append(c.MostExpensive, RuleCost{Path: path, Cost: cost})
|
||||
sort.Slice(c.MostExpensive, func(i, j int) bool {
|
||||
// sort in descending order so the most expensive rule is first
|
||||
return c.MostExpensive[i].Cost > c.MostExpensive[j].Cost
|
||||
})
|
||||
if len(c.MostExpensive) > 4 {
|
||||
c.MostExpensive = c.MostExpensive[:4]
|
||||
}
|
||||
}
|
||||
|
||||
// RuleCost represents the cost of evaluating a single x-kubernetes-validations rule.
|
||||
type RuleCost struct {
|
||||
Path *field.Path
|
||||
Cost uint64
|
||||
}
|
||||
|
||||
// extractMaxElements returns the factor by which the schema increases the cardinality
|
||||
// (number of possible data elements) of its children. If schema is a map and has
|
||||
// MaxProperties or an array has MaxItems, the int pointer of the max value is returned.
|
||||
// If schema is a map or array and does not have MaxProperties or MaxItems,
|
||||
// unbounded (nil) is returned to indicate that there is no limit to the possible
|
||||
// number of data elements imposed by the current schema. If the schema is an object, 1 is
|
||||
// returned to indicate that there is no increase to the number of possible data elements
|
||||
// for its children. Primitives do not have children, but 1 is returned for simplicity.
|
||||
func extractMaxElements(schema *apiextensions.JSONSchemaProps) *uint64 {
|
||||
switch schema.Type {
|
||||
case "object":
|
||||
if schema.AdditionalProperties != nil {
|
||||
if schema.MaxProperties != nil {
|
||||
maxProps := uint64(zeroIfNegative(*schema.MaxProperties))
|
||||
return &maxProps
|
||||
}
|
||||
return unbounded
|
||||
}
|
||||
// return 1 to indicate that all fields of an object exist at most one for
|
||||
// each occurrence of the object they are fields of
|
||||
return uint64ptr(1)
|
||||
case "array":
|
||||
if schema.MaxItems != nil {
|
||||
maxItems := uint64(zeroIfNegative(*schema.MaxItems))
|
||||
return &maxItems
|
||||
}
|
||||
return unbounded
|
||||
default:
|
||||
return uint64ptr(1)
|
||||
}
|
||||
}
|
||||
|
||||
func zeroIfNegative(v int64) int64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func uint64ptr(i uint64) *uint64 {
|
||||
return &i
|
||||
}
|
@ -22,7 +22,6 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@ -723,17 +722,17 @@ func validateCustomResourceDefinitionValidation(ctx context.Context, customResou
|
||||
requireValidPropertyType: opts.requireValidPropertyType,
|
||||
}
|
||||
|
||||
costInfo := rootCostInfo()
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true, &opts, costInfo)...)
|
||||
celContext := RootCELContext(schema)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema, true, &opts, celContext)...)
|
||||
|
||||
if costInfo.TotalCost != nil {
|
||||
if costInfo.TotalCost.totalCost > StaticEstimatedCRDCostLimit {
|
||||
for _, expensive := range costInfo.TotalCost.mostExpensive {
|
||||
if celContext.TotalCost != nil {
|
||||
if celContext.TotalCost.Total > StaticEstimatedCRDCostLimit {
|
||||
for _, expensive := range celContext.TotalCost.MostExpensive {
|
||||
costErrorMsg := fmt.Sprintf("contributed to estimated rule cost total exceeding cost limit for entire OpenAPIv3 schema")
|
||||
allErrs = append(allErrs, field.Forbidden(expensive.path, costErrorMsg))
|
||||
allErrs = append(allErrs, field.Forbidden(expensive.Path, costErrorMsg))
|
||||
}
|
||||
|
||||
costErrorMsg := getCostErrorMessage("x-kubernetes-validations estimated rule cost total for entire OpenAPIv3 schema", costInfo.TotalCost.totalCost, StaticEstimatedCRDCostLimit)
|
||||
costErrorMsg := getCostErrorMessage("x-kubernetes-validations estimated rule cost total for entire OpenAPIv3 schema", celContext.TotalCost.Total, StaticEstimatedCRDCostLimit)
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("openAPIV3Schema"), costErrorMsg))
|
||||
}
|
||||
}
|
||||
@ -764,134 +763,10 @@ func validateCustomResourceDefinitionValidation(ctx context.Context, customResou
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// unbounded uses nil to represent an unbounded cardinality value.
|
||||
var unbounded *uint64 = nil
|
||||
|
||||
type costInfo struct {
|
||||
// MaxCardinality represents a limit to the number of data elements that can exist for the current
|
||||
// schema based on MaxProperties or MaxItems limits present on parent schemas, If all parent
|
||||
// map and array schemas have MaxProperties or MaxItems limits declared MaxCardinality is
|
||||
// an int pointer representing the product of these limits. If least one parent map or list schema
|
||||
// does not have a MaxProperties or MaxItems limits set, the MaxCardinality is nil, indicating
|
||||
// that the parent schemas offer no bound to the number of times a data element for the current
|
||||
// schema can exist.
|
||||
MaxCardinality *uint64
|
||||
// TotalCost accumulates the x-kubernetes-validators estimated rule cost total for an entire custom resource
|
||||
// definition. A single totalCost is allocated for each validation call and passed through the stack as the
|
||||
// custom resource definition's OpenAPIv3 schema is recursively validated.
|
||||
TotalCost *totalCost
|
||||
}
|
||||
|
||||
type totalCost struct {
|
||||
// totalCost accumulates the x-kubernetes-validators estimated rule cost total.
|
||||
totalCost uint64
|
||||
// mostExpensive accumulates the top 4 most expensive rules contributing to the totalCost. Only rules
|
||||
// that accumulate at least 1% of total cost limit are included.
|
||||
mostExpensive []ruleCost
|
||||
}
|
||||
|
||||
func (c *totalCost) observeExpressionCost(path *field.Path, cost uint64) {
|
||||
if math.MaxUint64-c.totalCost < cost {
|
||||
c.totalCost = math.MaxUint64
|
||||
} else {
|
||||
c.totalCost += cost
|
||||
}
|
||||
|
||||
if cost < StaticEstimatedCRDCostLimit/100 { // ignore rules that contribute < 1% of total cost limit
|
||||
return
|
||||
}
|
||||
c.mostExpensive = append(c.mostExpensive, ruleCost{path: path, cost: cost})
|
||||
sort.Slice(c.mostExpensive, func(i, j int) bool {
|
||||
// sort in descending order so the most expensive rule is first
|
||||
return c.mostExpensive[i].cost > c.mostExpensive[j].cost
|
||||
})
|
||||
if len(c.mostExpensive) > 4 {
|
||||
c.mostExpensive = c.mostExpensive[:4]
|
||||
}
|
||||
}
|
||||
|
||||
type ruleCost struct {
|
||||
path *field.Path
|
||||
cost uint64
|
||||
}
|
||||
|
||||
// MultiplyByElementCost returns a costInfo where the MaxCardinality is multiplied by the
|
||||
// factor that the schema increases the cardinality of its children. If the costInfo's
|
||||
// MaxCardinality is unbounded (nil) or the factor that the schema increase the cardinality
|
||||
// is unbounded, the resulting costInfo's MaxCardinality is also unbounded.
|
||||
func (c *costInfo) MultiplyByElementCost(schema *apiextensions.JSONSchemaProps) costInfo {
|
||||
result := costInfo{TotalCost: c.TotalCost, MaxCardinality: unbounded}
|
||||
if schema == nil {
|
||||
// nil schemas can be passed since we call MultiplyByElementCost
|
||||
// before ValidateCustomResourceDefinitionOpenAPISchema performs its nil check
|
||||
return result
|
||||
}
|
||||
if c.MaxCardinality == unbounded {
|
||||
return result
|
||||
}
|
||||
maxElements := extractMaxElements(schema)
|
||||
if maxElements == unbounded {
|
||||
return result
|
||||
}
|
||||
result.MaxCardinality = uint64ptr(multiplyWithOverflowGuard(*c.MaxCardinality, *maxElements))
|
||||
return result
|
||||
}
|
||||
|
||||
// extractMaxElements returns the factor by which the schema increases the cardinality
|
||||
// (number of possible data elements) of its children. If schema is a map and has
|
||||
// MaxProperties or an array has MaxItems, the int pointer of the max value is returned.
|
||||
// If schema is a map or array and does not have MaxProperties or MaxItems,
|
||||
// unbounded (nil) is returned to indicate that there is no limit to the possible
|
||||
// number of data elements imposed by the current schema. If the schema is an object, 1 is
|
||||
// returned to indicate that there is no increase to the number of possible data elements
|
||||
// for its children. Primitives do not have children, but 1 is returned for simplicity.
|
||||
func extractMaxElements(schema *apiextensions.JSONSchemaProps) *uint64 {
|
||||
switch schema.Type {
|
||||
case "object":
|
||||
if schema.AdditionalProperties != nil {
|
||||
if schema.MaxProperties != nil {
|
||||
maxProps := uint64(zeroIfNegative(*schema.MaxProperties))
|
||||
return &maxProps
|
||||
}
|
||||
return unbounded
|
||||
}
|
||||
// return 1 to indicate that all fields of an object exist at most one for
|
||||
// each occurrence of the object they are fields of
|
||||
return uint64ptr(1)
|
||||
case "array":
|
||||
if schema.MaxItems != nil {
|
||||
maxItems := uint64(zeroIfNegative(*schema.MaxItems))
|
||||
return &maxItems
|
||||
}
|
||||
return unbounded
|
||||
default:
|
||||
return uint64ptr(1)
|
||||
}
|
||||
}
|
||||
|
||||
func zeroIfNegative(v int64) int64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func uint64ptr(i uint64) *uint64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func rootCostInfo() costInfo {
|
||||
rootCardinality := uint64(1)
|
||||
return costInfo{
|
||||
MaxCardinality: &rootCardinality,
|
||||
TotalCost: &totalCost{},
|
||||
}
|
||||
}
|
||||
|
||||
var metaFields = sets.NewString("metadata", "kind", "apiVersion")
|
||||
|
||||
// ValidateCustomResourceDefinitionOpenAPISchema statically validates
|
||||
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator, isRoot bool, opts *validationOptions, nodeCostInfo costInfo) field.ErrorList {
|
||||
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator, isRoot bool, opts *validationOptions, celContext *CELSchemaContext) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if schema == nil {
|
||||
@ -925,7 +800,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
// we have to forbid defaults inside additionalProperties because pruning without actual value is ambiguous
|
||||
subSsv = ssv.withForbiddenDefaults("inside additionalProperties applying to object metadata")
|
||||
}
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), subSsv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), subSsv, false, opts, celContext.ChildAdditionalPropertiesContext(schema.AdditionalProperties.Schema))...)
|
||||
}
|
||||
|
||||
if len(schema.Properties) != 0 {
|
||||
@ -943,33 +818,38 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
subSsv = subSsv.withForbiddenDefaults(fmt.Sprintf("in top-level %s", property))
|
||||
}
|
||||
}
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), subSsv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
propertySchema := jsonSchema
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&propertySchema, fldPath.Child("properties").Key(property), subSsv, false, opts, celContext.ChildPropertyContext(&propertySchema, property))...)
|
||||
}
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv, false, opts, nil)...)
|
||||
|
||||
if len(schema.AllOf) != 0 {
|
||||
for i, jsonSchema := range schema.AllOf {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf").Index(i), ssv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
allOfSchema := jsonSchema
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&allOfSchema, fldPath.Child("allOf").Index(i), ssv, false, opts, nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(schema.OneOf) != 0 {
|
||||
for i, jsonSchema := range schema.OneOf {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf").Index(i), ssv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
oneOfSchema := jsonSchema
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&oneOfSchema, fldPath.Child("oneOf").Index(i), ssv, false, opts, nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(schema.AnyOf) != 0 {
|
||||
for i, jsonSchema := range schema.AnyOf {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf").Index(i), ssv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
anyOfSchema := jsonSchema
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&anyOfSchema, fldPath.Child("anyOf").Index(i), ssv, false, opts, nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(schema.Definitions) != 0 {
|
||||
for definition, jsonSchema := range schema.Definitions {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
definitionSchema := jsonSchema
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&definitionSchema, fldPath.Child("definitions").Key(definition), ssv, false, opts, nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
@ -983,17 +863,18 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
subSsv = subSsv.withForbidOldSelfValidations(fldPath)
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), subSsv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), subSsv, false, opts, celContext.ChildItemsContext(schema.Items.Schema))...)
|
||||
if len(schema.Items.JSONSchemas) != 0 {
|
||||
for i, jsonSchema := range schema.Items.JSONSchemas {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items").Index(i), subSsv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
itemsSchema := jsonSchema
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&itemsSchema, fldPath.Child("items").Index(i), subSsv, false, opts, celContext.ChildItemsContext(&itemsSchema))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if schema.Dependencies != nil {
|
||||
for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv, false, opts, nodeCostInfo.MultiplyByElementCost(schema))...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv, false, opts, nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1095,31 +976,37 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
}
|
||||
}
|
||||
|
||||
structural, err := structuralschema.NewStructural(schema)
|
||||
if err == nil {
|
||||
compResults, err := cel.Compile(structural, isRoot, cel.PerCallLimit)
|
||||
if celContext != nil {
|
||||
typeInfo, err := celContext.TypeInfo()
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.InternalError(fldPath.Child("x-kubernetes-validations"), err))
|
||||
allErrs = append(allErrs, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to construct type information for x-kubernetes-validations rules: %s", err)))
|
||||
} else if typeInfo == nil {
|
||||
allErrs = append(allErrs, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to retrieve type information for x-kubernetes-validations")))
|
||||
} else {
|
||||
for i, cr := range compResults {
|
||||
expressionCost := getExpressionCost(cr, nodeCostInfo)
|
||||
if expressionCost > StaticEstimatedCostLimit {
|
||||
costErrorMsg := getCostErrorMessage("estimated rule cost", expressionCost, StaticEstimatedCostLimit)
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), costErrorMsg))
|
||||
}
|
||||
if nodeCostInfo.TotalCost != nil {
|
||||
nodeCostInfo.TotalCost.observeExpressionCost(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), expressionCost)
|
||||
}
|
||||
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))
|
||||
compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, cel.PerCallLimit)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.InternalError(fldPath.Child("x-kubernetes-validations"), err))
|
||||
} else {
|
||||
for i, cr := range compResults {
|
||||
expressionCost := getExpressionCost(cr, celContext)
|
||||
if expressionCost > StaticEstimatedCostLimit {
|
||||
costErrorMsg := getCostErrorMessage("estimated rule cost", expressionCost, StaticEstimatedCostLimit)
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), costErrorMsg))
|
||||
}
|
||||
}
|
||||
if cr.TransitionRule {
|
||||
if uncorrelatablePath := ssv.forbidOldSelfValidations(); uncorrelatablePath != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i].Rule, fmt.Sprintf("oldSelf cannot be used on the uncorrelatable portion of the schema within %v", uncorrelatablePath)))
|
||||
if celContext.TotalCost != nil {
|
||||
celContext.TotalCost.ObserveExpressionCost(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), expressionCost)
|
||||
}
|
||||
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 cr.TransitionRule {
|
||||
if uncorrelatablePath := ssv.forbidOldSelfValidations(); uncorrelatablePath != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i].Rule, fmt.Sprintf("oldSelf cannot be used on the uncorrelatable portion of the schema within %v", uncorrelatablePath)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1146,7 +1033,7 @@ func multiplyWithOverflowGuard(baseCost, cardinality uint64) uint64 {
|
||||
return baseCost * cardinality
|
||||
}
|
||||
|
||||
func getExpressionCost(cr cel.CompilationResult, cardinalityCost costInfo) uint64 {
|
||||
func getExpressionCost(cr cel.CompilationResult, cardinalityCost *CELSchemaContext) uint64 {
|
||||
if cardinalityCost.MaxCardinality != unbounded {
|
||||
return multiplyWithOverflowGuard(cr.MaxCost, *cardinalityCost.MaxCardinality)
|
||||
}
|
||||
|
@ -8151,6 +8151,249 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
||||
forbidden("spec.validation.openAPIV3Schema"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated for escaped property name",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f/2": {
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self.f__slash__2 == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under array items",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"a": {
|
||||
Type: "array",
|
||||
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[a].items.x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under array items, parent has rule",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"a": {Type: "array",
|
||||
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "1 == 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[a].items.x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under additionalProperties",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"a": {
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[a].additionalProperties.x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under additionalProperties, parent has rule",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"a": {
|
||||
Type: "object",
|
||||
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "1 == 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[a].additionalProperties.x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under unescaped property name",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f": {
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[f].x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under unescaped property name, parent has rule",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f": {
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "1 == 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[f].x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under escaped property name",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f/2": {
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[f/2].x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under escaped property name, parent has rule",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f/2": {
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "1 == 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[f/2].x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under unescapable property name",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f@2": {
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[f@2].x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "x-kubernetes-validations rule validated under unescapable property name, parent has rule",
|
||||
input: apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"f@2": {
|
||||
Type: "string",
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "self == 1"}, // invalid comparison of string and int
|
||||
},
|
||||
},
|
||||
},
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "1 == 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: []validationMatch{
|
||||
invalid("spec.validation.openAPIV3Schema.properties[f@2].x-kubernetes-validations[0].rule"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -8506,10 +8749,11 @@ func TestCostInfo(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
curCostInfo := rootCostInfo()
|
||||
// simulate the recursive validation calls
|
||||
for _, schema := range tt.schema {
|
||||
curCostInfo = curCostInfo.MultiplyByElementCost(schema)
|
||||
schemas := append(tt.schema, &apiextensions.JSONSchemaProps{Type: "string"}) // append a leaf type
|
||||
curCostInfo := RootCELContext(schemas[0])
|
||||
for i := 1; i < len(schemas); i++ {
|
||||
curCostInfo = curCostInfo.childContext(schemas[i], nil)
|
||||
}
|
||||
if tt.expectedMaxCardinality == nil && curCostInfo.MaxCardinality == nil {
|
||||
// unbounded cardinality case, test ran correctly
|
||||
@ -8524,6 +8768,63 @@ func TestCostInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCelContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *apiextensions.JSONSchemaProps
|
||||
}{
|
||||
{
|
||||
name: "verify that schemas are converted only once and then reused",
|
||||
schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
XValidations: []apiextensions.ValidationRule{{Rule: "self.size() < 100"}},
|
||||
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "array",
|
||||
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
XValidations: []apiextensions.ValidationRule{{Rule: "has(self.field)"}},
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
XValidations: []apiextensions.ValidationRule{{Rule: "self.startsWith('abc')"}},
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// simulate the recursive validation calls
|
||||
conversionCount := 0
|
||||
converter := func(schema *apiextensions.JSONSchemaProps, isRoot bool) (*CELTypeInfo, error) {
|
||||
conversionCount++
|
||||
return defaultConverter(schema, isRoot)
|
||||
}
|
||||
celContext := RootCELContext(tt.schema)
|
||||
celContext.converter = converter
|
||||
opts := validationOptions{}
|
||||
openAPIV3Schema := &specStandardValidatorV3{
|
||||
allowDefaults: opts.allowDefaults,
|
||||
disallowDefaultsReason: opts.disallowDefaultsReason,
|
||||
requireValidPropertyType: opts.requireValidPropertyType,
|
||||
}
|
||||
errors := ValidateCustomResourceDefinitionOpenAPISchema(tt.schema, field.NewPath("openAPIV3Schema"), openAPIV3Schema, true, &opts, celContext)
|
||||
if len(errors) != 0 {
|
||||
t.Errorf("Expected no validate errors but got %v", errors)
|
||||
}
|
||||
if conversionCount != 1 {
|
||||
t.Errorf("Expected 1 conversion to be performed by cel context during schema traversal but observed %d conversions", conversionCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerCRDEstimatedCost(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -8582,20 +8883,20 @@ func TestPerCRDEstimatedCost(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
crdCost := rootCostInfo().TotalCost
|
||||
crdCost := TotalCost{}
|
||||
for _, cost := range tt.costs {
|
||||
crdCost.observeExpressionCost(nil, cost)
|
||||
crdCost.ObserveExpressionCost(nil, cost)
|
||||
}
|
||||
if len(crdCost.mostExpensive) != len(tt.expectedExpensive) {
|
||||
t.Fatalf("expected %d largest costs but got %d: %v", len(tt.expectedExpensive), len(crdCost.mostExpensive), crdCost.mostExpensive)
|
||||
if len(crdCost.MostExpensive) != len(tt.expectedExpensive) {
|
||||
t.Fatalf("expected %d largest costs but got %d: %v", len(tt.expectedExpensive), len(crdCost.MostExpensive), crdCost.MostExpensive)
|
||||
}
|
||||
for i, expensive := range crdCost.mostExpensive {
|
||||
if tt.expectedExpensive[i] != expensive.cost {
|
||||
t.Errorf("expected largest cost of %d at index %d but got %d", tt.expectedExpensive[i], i, expensive.cost)
|
||||
for i, expensive := range crdCost.MostExpensive {
|
||||
if tt.expectedExpensive[i] != expensive.Cost {
|
||||
t.Errorf("expected largest cost of %d at index %d but got %d", tt.expectedExpensive[i], i, expensive.Cost)
|
||||
}
|
||||
}
|
||||
if tt.expectedTotal != crdCost.totalCost {
|
||||
t.Errorf("expected total cost of %d but got %d", tt.expectedTotal, crdCost.totalCost)
|
||||
if tt.expectedTotal != crdCost.Total {
|
||||
t.Errorf("expected total cost of %d but got %d", tt.expectedTotal, crdCost.Total)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1094,7 +1094,7 @@ func TestCelCostStability(t *testing.T) {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := withRule(*tt.schema, validRule)
|
||||
celValidator := NewValidator(&s, PerCallLimit)
|
||||
celValidator := NewValidator(&s, true, PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
|
@ -89,16 +89,16 @@ func getBaseEnv() (*cel.Env, 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.
|
||||
// CompilationResult for each ValidationRule, or an error. declType is expected to be a CEL DeclType corresponding
|
||||
// to the structural schema.
|
||||
// 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
|
||||
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit as input.
|
||||
func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([]CompilationResult, error) {
|
||||
func Compile(s *schema.Structural, declType *celmodel.DeclType, perCallLimit uint64) ([]CompilationResult, error) {
|
||||
t := time.Now()
|
||||
defer metrics.Metrics.ObserveCompilation(time.Since(t))
|
||||
|
||||
if len(s.Extensions.XValidations) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@ -113,7 +113,7 @@ func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([]
|
||||
}
|
||||
reg := celmodel.NewRegistry(baseEnv)
|
||||
scopedTypeName := generateUniqueSelfTypeName()
|
||||
rt, err := celmodel.NewRuleTypes(scopedTypeName, s, isResourceRoot, reg)
|
||||
rt, err := celmodel.NewRuleTypes(scopedTypeName, declType, reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -126,11 +126,10 @@ func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([]
|
||||
}
|
||||
root, ok = rt.FindDeclType(scopedTypeName)
|
||||
if !ok {
|
||||
rootDecl := celmodel.SchemaDeclType(s, isResourceRoot)
|
||||
if rootDecl == nil {
|
||||
if declType == 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)
|
||||
root = declType.MaybeAssignTypeName(scopedTypeName)
|
||||
}
|
||||
propDecls = append(propDecls, cel.Variable(ScopedVarName, root.CelType()))
|
||||
propDecls = append(propDecls, cel.Variable(OldScopedVarName, root.CelType()))
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
|
||||
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -642,7 +643,7 @@ func TestCelCompilation(t *testing.T) {
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
compilationResults, err := Compile(&tt.input, false, PerCallLimit)
|
||||
compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), PerCallLimit)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got: %v", err)
|
||||
}
|
||||
@ -1078,7 +1079,7 @@ func genMapWithCustomItemRule(item *schema.Structural, rule string) func(maxProp
|
||||
// if expectedCostExceedsLimit is non-zero. Typically, only expectedCost or expectedCostExceedsLimit is non-zero, not both.
|
||||
func schemaChecker(schema *schema.Structural, expectedCost uint64, expectedCostExceedsLimit uint64, t *testing.T) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
compilationResults, err := Compile(schema, false, PerCallLimit)
|
||||
compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), PerCallLimit)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
}
|
||||
@ -1614,7 +1615,7 @@ func BenchmarkCompile(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := Compile(s, false, uint64(math.MaxInt64))
|
||||
_, err := Compile(s, model.SchemaDeclType(s, false), uint64(math.MaxInt64))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
@ -65,27 +65,47 @@ type Validator struct {
|
||||
// Returns nil only if there no validator rules in the Structural schema. May return a validator containing
|
||||
// only errors.
|
||||
// Adding perCallLimit as input arg for testing purpose only. Callers should always use const PerCallLimit as input
|
||||
func NewValidator(s *schema.Structural, perCallLimit uint64) *Validator {
|
||||
return validator(s, true, perCallLimit)
|
||||
func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) *Validator {
|
||||
return validator(s, true, model.SchemaDeclType(s, isResourceRoot), perCallLimit)
|
||||
}
|
||||
|
||||
func validator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) *Validator {
|
||||
compiledRules, err := Compile(s, isResourceRoot, perCallLimit)
|
||||
// validator creates a Validator for all x-kubernetes-validations at the level of the provided schema and lower and
|
||||
// returns the Validator if any x-kubernetes-validations exist in the schema, or nil if no x-kubernetes-validations
|
||||
// exist. declType is expected to be a CEL DeclType corresponding to the structural schema.
|
||||
func validator(s *schema.Structural, isResourceRoot bool, declType *model.DeclType, perCallLimit uint64) *Validator {
|
||||
compiledRules, err := Compile(s, declType, perCallLimit)
|
||||
var itemsValidator, additionalPropertiesValidator *Validator
|
||||
var propertiesValidators map[string]Validator
|
||||
if s.Items != nil {
|
||||
itemsValidator = validator(s.Items, s.Items.XEmbeddedResource, perCallLimit)
|
||||
itemsValidator = validator(s.Items, s.Items.XEmbeddedResource, declType.ElemType, perCallLimit)
|
||||
}
|
||||
if len(s.Properties) > 0 {
|
||||
propertiesValidators = make(map[string]Validator, len(s.Properties))
|
||||
for k, prop := range s.Properties {
|
||||
if p := validator(&prop, prop.XEmbeddedResource, perCallLimit); p != nil {
|
||||
for k, p := range s.Properties {
|
||||
prop := p
|
||||
var fieldType *model.DeclType
|
||||
if escapedPropName, ok := model.Escape(k); ok {
|
||||
if f, ok := declType.Fields[escapedPropName]; ok {
|
||||
fieldType = f.Type
|
||||
} else {
|
||||
// fields with unknown types are omitted from CEL validation entirely
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// field may be absent from declType if the property name is unescapable, in which case we should convert
|
||||
// the field value type to a DeclType.
|
||||
fieldType = model.SchemaDeclType(&prop, prop.XEmbeddedResource)
|
||||
if fieldType == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if p := validator(&prop, prop.XEmbeddedResource, fieldType, perCallLimit); p != nil {
|
||||
propertiesValidators[k] = *p
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
|
||||
additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, perCallLimit)
|
||||
additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit)
|
||||
}
|
||||
if len(compiledRules) > 0 || err != nil || itemsValidator != nil || additionalPropertiesValidator != nil || len(propertiesValidators) > 0 {
|
||||
var activationFactory func(*schema.Structural, interface{}, interface{}) interpreter.Activation = validationActivationWithoutOldSelf
|
||||
|
@ -24,10 +24,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kube-openapi/pkg/validation/strfmt"
|
||||
|
||||
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"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/kube-openapi/pkg/validation/strfmt"
|
||||
)
|
||||
|
||||
// TestValidationExpressions tests CEL integration with custom resource values and OpenAPIv3.
|
||||
@ -1767,7 +1769,7 @@ func TestValidationExpressions(t *testing.T) {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := withRule(*tt.schema, validRule)
|
||||
celValidator := validator(&s, tt.isRoot, PerCallLimit)
|
||||
celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -1791,7 +1793,7 @@ func TestValidationExpressions(t *testing.T) {
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
s := withRule(*tt.schema, rule)
|
||||
celValidator := NewValidator(&s, PerCallLimit)
|
||||
celValidator := NewValidator(&s, true, PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -1810,6 +1812,181 @@ func TestValidationExpressions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidationExpressionsInSchema tests CEL integration with custom resource values and OpenAPIv3 for cases
|
||||
// where the validation rules are defined at any level within the schema.
|
||||
func TestValidationExpressionsAtSchemaLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *schema.Structural
|
||||
obj interface{}
|
||||
oldObj interface{}
|
||||
errors []string // strings that error message must contain
|
||||
}{
|
||||
{name: "invalid rule under array items",
|
||||
obj: map[string]interface{}{
|
||||
"f": []interface{}{1},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f": listType(cloneWithRule(&integerType, "self == 'abc'")),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under array items, parent has rule",
|
||||
obj: map[string]interface{}{
|
||||
"f": []interface{}{1},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f": withRule(listType(cloneWithRule(&integerType, "self == 'abc'")), "1 == 1"),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under additionalProperties",
|
||||
obj: map[string]interface{}{
|
||||
"f": map[string]interface{}{"k": 1},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f": mapType(cloneWithRule(&integerType, "self == 'abc'")),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under additionalProperties, parent has rule",
|
||||
obj: map[string]interface{}{
|
||||
"f": map[string]interface{}{"k": 1},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f": withRule(mapType(cloneWithRule(&integerType, "self == 'abc'")), "1 == 1"),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under unescaped field name",
|
||||
obj: map[string]interface{}{
|
||||
"f": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under unescaped field name, parent has rule",
|
||||
obj: map[string]interface{}{
|
||||
"f": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
|
||||
"f": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
|
||||
}), "1 == 1"),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
// check that escaped field names do not impact CEL rule validation
|
||||
{name: "invalid rule under escaped field name",
|
||||
obj: map[string]interface{}{
|
||||
"f/2": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under escaped field name, parent has rule",
|
||||
obj: map[string]interface{}{
|
||||
"f/2": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
|
||||
"f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
|
||||
}), "1 == 1"),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "failing rule under escaped field name",
|
||||
obj: map[string]interface{}{
|
||||
"f/2": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2"),
|
||||
}),
|
||||
errors: []string{"Invalid value: \"object\": failed rule: self.m == 2"},
|
||||
},
|
||||
// unescapable field names that are not accessed by the CEL rule are allowed and should not impact CEL rule validation
|
||||
{name: "invalid rule under unescapable field name",
|
||||
obj: map[string]interface{}{
|
||||
"a@b": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"a@b": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
|
||||
}),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "invalid rule under unescapable field name, parent has rule",
|
||||
obj: map[string]interface{}{
|
||||
"f@2": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
|
||||
"f@2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
|
||||
}), "1 == 1"),
|
||||
errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
|
||||
},
|
||||
{name: "failing rule under unescapable field name",
|
||||
obj: map[string]interface{}{
|
||||
"a@b": map[string]interface{}{
|
||||
"m": 1,
|
||||
},
|
||||
},
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"a@b": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2"),
|
||||
}),
|
||||
errors: []string{"Invalid value: \"object\": failed rule: self.m == 2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, tt.oldObj, math.MaxInt)
|
||||
unmatched := map[string]struct{}{}
|
||||
for _, e := range tt.errors {
|
||||
unmatched[e] = struct{}{}
|
||||
}
|
||||
for _, err := range errs {
|
||||
if err.Type != field.ErrorTypeInvalid {
|
||||
t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for expected := range unmatched {
|
||||
if strings.Contains(err.Error(), expected) {
|
||||
delete(unmatched, expected)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
|
||||
}
|
||||
}
|
||||
if len(unmatched) > 0 {
|
||||
t.Errorf("expected errors %v", unmatched)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCELValidationLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -1833,7 +2010,7 @@ func TestCELValidationLimit(t *testing.T) {
|
||||
t.Run(validRule, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := withRule(*tt.schema, validRule)
|
||||
celValidator := validator(&s, false, PerCallLimit)
|
||||
celValidator := validator(&s, false, model.SchemaDeclType(&s, false), PerCallLimit)
|
||||
|
||||
// test with cost budget exceeded
|
||||
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0)
|
||||
@ -1854,7 +2031,7 @@ func TestCELValidationLimit(t *testing.T) {
|
||||
|
||||
// test with PerCallLimit exceeded
|
||||
found = false
|
||||
celValidator = NewValidator(&s, 0)
|
||||
celValidator = NewValidator(&s, true, 0)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -1901,7 +2078,7 @@ func TestCELValidationContextCancellation(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
s := withRule(*tt.schema, tt.rule)
|
||||
celValidator := NewValidator(&s, PerCallLimit)
|
||||
celValidator := NewValidator(&s, true, PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -1967,7 +2144,7 @@ func TestCELMaxRecursionDepth(t *testing.T) {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := withRule(*tt.schema, validRule)
|
||||
celValidator := validator(&s, tt.isRoot, PerCallLimit)
|
||||
celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -1991,7 +2168,7 @@ func TestCELMaxRecursionDepth(t *testing.T) {
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
s := withRule(*tt.schema, rule)
|
||||
celValidator := NewValidator(&s, PerCallLimit)
|
||||
celValidator := NewValidator(&s, true, PerCallLimit)
|
||||
if celValidator == nil {
|
||||
t.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -2036,7 +2213,7 @@ func BenchmarkCELValidationWithContext(b *testing.B) {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
ctx := context.TODO()
|
||||
s := withRule(*tt.schema, tt.rule)
|
||||
celValidator := NewValidator(&s, PerCallLimit)
|
||||
celValidator := NewValidator(&s, true, PerCallLimit)
|
||||
if celValidator == nil {
|
||||
b.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -2076,7 +2253,7 @@ func BenchmarkCELValidationWithCancelledContext(b *testing.B) {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
ctx := context.TODO()
|
||||
s := withRule(*tt.schema, tt.rule)
|
||||
celValidator := NewValidator(&s, PerCallLimit)
|
||||
celValidator := NewValidator(&s, true, PerCallLimit)
|
||||
if celValidator == nil {
|
||||
b.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -2130,7 +2307,7 @@ func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) {
|
||||
},
|
||||
},
|
||||
}
|
||||
validator := NewValidator(s, PerCallLimit)
|
||||
validator := NewValidator(s, true, PerCallLimit)
|
||||
if validator == nil {
|
||||
b.Fatal("expected non nil validator")
|
||||
}
|
||||
@ -2284,6 +2461,20 @@ func withRule(s schema.Structural, rule string) schema.Structural {
|
||||
return s
|
||||
}
|
||||
|
||||
func withRulePtr(s *schema.Structural, rule string) *schema.Structural {
|
||||
s.Extensions.XValidations = apiextensions.ValidationRules{
|
||||
{
|
||||
Rule: rule,
|
||||
},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func cloneWithRule(s *schema.Structural, rule string) *schema.Structural {
|
||||
s = s.DeepCopy()
|
||||
return withRulePtr(s, rule)
|
||||
}
|
||||
|
||||
func withMaxLength(s schema.Structural, maxLength *int64) schema.Structural {
|
||||
if s.ValueValidation == nil {
|
||||
s.ValueValidation = &schema.ValueValidation{}
|
||||
|
@ -70,6 +70,8 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
|
||||
rootSchema = s
|
||||
}
|
||||
|
||||
isResourceRoot := s == rootSchema
|
||||
|
||||
if s.Default.Object != nil {
|
||||
validator := kubeopenapivalidate.NewSchemaValidator(s.ToKubeOpenAPI(), nil, "", strfmt.Default)
|
||||
|
||||
@ -89,7 +91,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
|
||||
allErrs = append(allErrs, field.Invalid(pth.Child("default"), s.Default.Object, fmt.Sprintf("must result in valid metadata: %v", errs.ToAggregate())))
|
||||
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
|
||||
allErrs = append(allErrs, errs...)
|
||||
} else if celValidator := cel.NewValidator(s, cel.PerCallLimit); celValidator != nil {
|
||||
} else if celValidator := cel.NewValidator(s, isResourceRoot, cel.PerCallLimit); celValidator != nil {
|
||||
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
|
||||
remainingCost = rmCost
|
||||
allErrs = append(allErrs, celErrs...)
|
||||
@ -114,7 +116,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
|
||||
allErrs = append(allErrs, errs...)
|
||||
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
|
||||
allErrs = append(allErrs, errs...)
|
||||
} else if celValidator := cel.NewValidator(s, cel.PerCallLimit); celValidator != nil {
|
||||
} else if celValidator := cel.NewValidator(s, isResourceRoot, cel.PerCallLimit); celValidator != nil {
|
||||
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
|
||||
remainingCost = rmCost
|
||||
allErrs = append(allErrs, celErrs...)
|
||||
|
@ -29,6 +29,8 @@ import (
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
kjson "sigs.k8s.io/json"
|
||||
|
||||
kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
"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"
|
||||
@ -40,7 +42,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
// TestRoundTrip checks the conversion to go-openapi types.
|
||||
@ -163,6 +164,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
}{
|
||||
{name: "!nullable",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "object",
|
||||
@ -185,6 +187,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "nullable",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "object",
|
||||
@ -207,6 +210,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "nullable and no type",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Nullable: true,
|
||||
@ -226,6 +230,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "x-kubernetes-int-or-string",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
XIntOrString: true,
|
||||
@ -247,6 +252,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "nullable and x-kubernetes-int-or-string",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Nullable: true,
|
||||
@ -269,6 +275,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "nullable, x-kubernetes-int-or-string and user-provided anyOf",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Nullable: true,
|
||||
@ -311,6 +318,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "nullable, x-kubernetes-int-or-string and user-provider allOf",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Nullable: true,
|
||||
@ -361,6 +369,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "invalid regex",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "string",
|
||||
@ -374,6 +383,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "required field",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Required: []string{"field"},
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
@ -392,6 +402,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "enum",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "object",
|
||||
@ -424,6 +435,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "immutability transition rule",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "string",
|
||||
@ -453,6 +465,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
{name: "correlatable transition rule",
|
||||
// Ensures a transition rule under a "listMap" is supported.
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "array",
|
||||
@ -503,6 +516,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
// does NOT use oldSelf (is not a transition rule), still behaves
|
||||
// as expected under a non-correlatable field.
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"field": {
|
||||
Type: "array",
|
||||
@ -537,6 +551,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "maxProperties",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"fieldX": {
|
||||
Type: "object",
|
||||
@ -552,6 +567,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "maxItems",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"fieldX": {
|
||||
Type: "array",
|
||||
@ -567,6 +583,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
},
|
||||
{name: "maxLength",
|
||||
schema: apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"fieldX": {
|
||||
Type: "string",
|
||||
@ -591,7 +608,7 @@ func TestValidateCustomResource(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
celValidator := cel.NewValidator(structural, cel.PerCallLimit)
|
||||
celValidator := cel.NewValidator(structural, false, cel.PerCallLimit)
|
||||
for i, obj := range tt.objects {
|
||||
var oldObject interface{}
|
||||
if len(tt.oldObjects) == len(tt.objects) {
|
||||
|
@ -19,6 +19,8 @@ package customresource
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/kube-openapi/pkg/validation/validate"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||
@ -37,7 +39,6 @@ import (
|
||||
apiserverstorage "k8s.io/apiserver/pkg/storage"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kube-openapi/pkg/validation/validate"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
)
|
||||
@ -60,7 +61,7 @@ func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.Gr
|
||||
celValidators := map[string]*cel.Validator{}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) {
|
||||
for name, s := range structuralSchemas {
|
||||
v := cel.NewValidator(s, cel.PerCallLimit) // CEL programs are compiled and cached here
|
||||
v := cel.NewValidator(s, true, cel.PerCallLimit) // CEL programs are compiled and cached here
|
||||
if v != nil {
|
||||
celValidators[name] = v
|
||||
}
|
||||
|
@ -26,8 +26,6 @@ import (
|
||||
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -81,7 +79,8 @@ func newSimpleType(name string, celType *cel.Type, zeroVal ref.Val) *DeclType {
|
||||
type DeclType struct {
|
||||
fmt.Stringer
|
||||
|
||||
name string
|
||||
name string
|
||||
// Fields contains a map of escaped CEL identifier field names to field declarations.
|
||||
Fields map[string]*DeclField
|
||||
KeyType *DeclType
|
||||
ElemType *DeclType
|
||||
@ -295,14 +294,13 @@ func (f *DeclField) EnumValues() []ref.Val {
|
||||
|
||||
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
|
||||
func NewRuleTypes(kind string,
|
||||
schema *schema.Structural,
|
||||
isResourceRoot bool,
|
||||
declType *DeclType,
|
||||
res Resolver) (*RuleTypes, error) {
|
||||
// Note, if the schema indicates that it's actually based on another proto
|
||||
// then prefer the proto definition. For expressions in the proto, a new field
|
||||
// annotation will be needed to indicate the expected environment and type of
|
||||
// the expression.
|
||||
schemaTypes, err := newSchemaTypeProvider(kind, schema, isResourceRoot)
|
||||
schemaTypes, err := newSchemaTypeProvider(kind, declType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -310,7 +308,6 @@ func NewRuleTypes(kind string,
|
||||
return nil, nil
|
||||
}
|
||||
return &RuleTypes{
|
||||
Schema: schema,
|
||||
ruleSchemaDeclTypes: schemaTypes,
|
||||
resolver: res,
|
||||
}, nil
|
||||
@ -320,7 +317,6 @@ func NewRuleTypes(kind string,
|
||||
// type-system.
|
||||
type RuleTypes struct {
|
||||
ref.TypeProvider
|
||||
Schema *schema.Structural
|
||||
ruleSchemaDeclTypes *schemaTypeProvider
|
||||
typeAdapter ref.TypeAdapter
|
||||
resolver Resolver
|
||||
@ -345,7 +341,6 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
|
||||
rtWithTypes := &RuleTypes{
|
||||
TypeProvider: tp,
|
||||
typeAdapter: ta,
|
||||
Schema: rt.Schema,
|
||||
ruleSchemaDeclTypes: rt.ruleSchemaDeclTypes,
|
||||
resolver: rt.resolver,
|
||||
}
|
||||
@ -492,12 +487,11 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
|
||||
}
|
||||
}
|
||||
|
||||
func newSchemaTypeProvider(kind string, schema *schema.Structural, isResourceRoot bool) (*schemaTypeProvider, error) {
|
||||
delType := SchemaDeclType(schema, isResourceRoot)
|
||||
if delType == nil {
|
||||
func newSchemaTypeProvider(kind string, declType *DeclType) (*schemaTypeProvider, error) {
|
||||
if declType == nil {
|
||||
return nil, nil
|
||||
}
|
||||
root := delType.MaybeAssignTypeName(kind)
|
||||
root := declType.MaybeAssignTypeName(kind)
|
||||
types := FieldTypeMap(kind, root)
|
||||
return &schemaTypeProvider{
|
||||
root: root,
|
||||
|
@ -75,7 +75,7 @@ func TestTypes_MapType(t *testing.T) {
|
||||
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
|
||||
stdEnv, _ := cel.NewEnv()
|
||||
reg := NewRegistry(stdEnv)
|
||||
rt, err := NewRuleTypes("CustomObject", testSchema(), true, reg)
|
||||
rt, err := NewRuleTypes("CustomObject", SchemaDeclType(testSchema(), true), reg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user