diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go index a3804387f20..e405181ed56 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go @@ -22,6 +22,7 @@ import ( "time" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/checker/decls" expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "google.golang.org/protobuf/proto" @@ -54,10 +55,11 @@ const ( type CompilationResult struct { Program cel.Program Error *Error - // If true, the compiled expression contains a reference to the identifier "oldSelf", and its corresponding rule // is implicitly a transition rule. TransitionRule bool + // Represents the worst-case cost of the compiled expression in terms of CEL's cost units, as used by cel.EstimateCost. + MaxCost uint64 } // Compile compiles all the XValidations rules (without recursing into the schema) and returns a slice containing a @@ -111,17 +113,17 @@ func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([] if err != nil { return nil, err } - + estimator := celEstimator{root: root} // 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 { - compResults[i] = compileRule(rule, env, perCallLimit) + compResults[i] = compileRule(rule, env, perCallLimit, &estimator) } return compResults, nil } -func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit uint64) (compilationResult CompilationResult) { +func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit uint64, estimator *celEstimator) (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 @@ -156,7 +158,12 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u compilationResult.Error = &Error{ErrorTypeInvalid, "program instantiation failed: " + err.Error()} return } - + costEst, err := env.EstimateCost(ast, estimator) + if err != nil { + compilationResult.Error = &Error{ErrorTypeInternal, "cost estimation failed: " + err.Error()} + return + } + compilationResult.MaxCost = costEst.Max compilationResult.Program = prog return } @@ -168,3 +175,45 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u func generateUniqueSelfTypeName() string { return fmt.Sprintf("selfType%d", time.Now().Nanosecond()) } + +type celEstimator struct { + root *celmodel.DeclType +} + +func (c *celEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstimate { + if len(element.Path()) == 0 { + // Path() can return an empty list, early exit if it does since we can't + // provide size estimates when that happens + return nil + } + currentNode := c.root + // cut off "self" from path, since we always start there + for _, name := range element.Path()[1:] { + switch name { + case "@items", "@values": + if currentNode.ElemType == nil { + return nil + } + currentNode = currentNode.ElemType + case "@keys": + if currentNode.KeyType == nil { + return nil + } + currentNode = currentNode.KeyType + default: + field, ok := currentNode.Fields[name] + if !ok { + return nil + } + if field.Type == nil { + return nil + } + currentNode = field.Type + } + } + return &checker.SizeEstimate{Min: 0, Max: uint64(currentNode.MaxElements)} +} + +func (c *celEstimator) EstimateCallCost(function, overloadID string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + return nil +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go index 9f418ad21d2..112b1d4ea76 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go @@ -18,6 +18,7 @@ package cel import ( "fmt" + "math" "strings" "testing" @@ -25,6 +26,10 @@ import ( "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" ) +const ( + costLimit = 100000000 +) + type validationMatcher interface { matches(cr CompilationResult) bool String() string @@ -658,3 +663,864 @@ func TestCelCompilation(t *testing.T) { }) } } + +// take a single rule type in (string/number/map/etc.) and return appropriate values for +// Type, Format, and XIntOrString +func parseRuleType(ruleType string) (string, string, bool) { + if ruleType == "duration" || ruleType == "date" || ruleType == "date-time" { + return "string", ruleType, false + } + if ruleType == "int-or-string" { + return "", "", true + } + return ruleType, "", false +} + +func genArrayWithRule(arrayType, rule string) func(maxItems *int64) *schema.Structural { + passedType, passedFormat, xIntString := parseRuleType(arrayType) + return func(maxItems *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: passedType, + }, + ValueValidation: &schema.ValueValidation{ + Format: passedFormat, + }, + Extensions: schema.Extensions{ + XIntOrString: xIntString, + }, + }, + ValueValidation: &schema.ValueValidation{ + MaxItems: maxItems, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genArrayOfArraysWithRule(arrayType, rule string) func(maxItems *int64) *schema.Structural { + return func(maxItems *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: arrayType, + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + MaxItems: maxItems, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genObjectArrayWithRule(rule string) func(maxItems *int64) *schema.Structural { + return func(maxItems *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "required": { + Generic: schema.Generic{ + Type: "string", + }, + }, + "optional": { + Generic: schema.Generic{ + Type: "string", + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + Required: []string{"required"}, + }, + }, + ValueValidation: &schema.ValueValidation{ + MaxItems: maxItems, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func getMapArrayWithRule(mapType, rule string) func(maxItems *int64) *schema.Structural { + return func(maxItems *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: mapType, + }, + }}, + }, + }, + ValueValidation: &schema.ValueValidation{ + MaxItems: maxItems, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genMapWithRule(mapType, rule string) func(maxProperties *int64) *schema.Structural { + passedType, passedFormat, xIntString := parseRuleType(mapType) + return func(maxProperties *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: passedType, + }, + ValueValidation: &schema.ValueValidation{ + Format: passedFormat, + }, + Extensions: schema.Extensions{ + XIntOrString: xIntString, + }, + }}, + }, + ValueValidation: &schema.ValueValidation{ + MaxProperties: maxProperties, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genStringWithRule(rule string) func(maxLength *int64) *schema.Structural { + return func(maxLength *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + MaxLength: maxLength, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genBytesWithRule(rule string) func(maxLength *int64) *schema.Structural { + return func(maxLength *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + MaxLength: maxLength, + Format: "byte", + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genNestedSpecWithRule(rule string) func(maxLength *int64) *schema.Structural { + return func(maxLength *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + MaxLength: maxLength, + }, + }}, + }, + ValueValidation: &schema.ValueValidation{ + MaxProperties: maxLength, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genAllMaxNestedSpecWithRootRule(rule string) func(maxLength *int64) *schema.Structural { + return func(maxLength *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + ValueValidation: &schema.ValueValidation{ + Required: []string{"required"}, + MaxProperties: maxLength, + }, + Properties: map[string]schema.Structural{ + "required": { + Generic: schema.Generic{ + Type: "string", + }, + }, + "optional": { + Generic: schema.Generic{ + Type: "string", + }, + }, + }, + }}, + }, + ValueValidation: &schema.ValueValidation{ + MaxProperties: maxLength, + }, + }, + ValueValidation: &schema.ValueValidation{ + MaxItems: maxLength, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genOneMaxNestedSpecWithRootRule(rule string) func(maxLength *int64) *schema.Structural { + return func(maxLength *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + ValueValidation: &schema.ValueValidation{ + Required: []string{"required"}, + }, + Properties: map[string]schema.Structural{ + "required": { + Generic: schema.Generic{ + Type: "string", + }, + }, + "optional": { + Generic: schema.Generic{ + Type: "string", + }, + }, + }, + }}, + }, + ValueValidation: &schema.ValueValidation{ + MaxProperties: maxLength, + }, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func genObjectForMap() *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "required": { + Generic: schema.Generic{ + Type: "string", + }, + }, + "optional": { + Generic: schema.Generic{ + Type: "string", + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + Required: []string{"required"}, + }, + } +} + +func genArrayForMap() *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "number", + }, + }, + } +} + +func genMapForMap() *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: "number", + }, + }}, + }, + } +} + +func genMapWithCustomItemRule(item *schema.Structural, rule string) func(maxProperties *int64) *schema.Structural { + return func(maxProperties *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: item}, + }, + ValueValidation: &schema.ValueValidation{ + MaxProperties: maxProperties, + }, + Extensions: schema.Extensions{ + XValidations: apiextensions.ValidationRules{ + { + Rule: rule, + }, + }, + }, + } + } +} + +func schemaChecker(schema *schema.Structural, expectedCost uint64, calcLimit uint64, t *testing.T) func(t *testing.T) { + return func(t *testing.T) { + // TODO(DangerOnTheRanger): if perCallLimit in compilation.go changes, this needs to change as well + compilationResults, err := Compile(schema, false, uint64(math.MaxInt64)) + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + if len(compilationResults) != 1 { + t.Errorf("Expected one rule, got: %d", len(compilationResults)) + } + result := compilationResults[0] + if result.Error != nil { + t.Errorf("Expected no compile-time error, got: %v", result.Error) + } + if calcLimit == 0 { + if result.MaxCost != expectedCost { + t.Errorf("Wrong cost (expected %d, got %d)", expectedCost, result.MaxCost) + } + } else { + if result.MaxCost < calcLimit { + t.Errorf("Cost did not exceed limit as expected (expected more than %d, got %d)", calcLimit, result.MaxCost) + } + } + } +} + +func TestCostEstimation(t *testing.T) { + cases := []struct { + name string + schemaGenerator func(maxLength *int64) *schema.Structural + expectedCalcCost uint64 + setMaxElements int64 + expectedSetCost uint64 + expectCalcCostExceedsLimit uint64 + }{ + { + name: "number array with all", + schemaGenerator: genArrayWithRule("number", "self.all(x, true)"), + expectedCalcCost: 4718591, + setMaxElements: 10, + expectedSetCost: 32, + }, + { + name: "string array with all", + schemaGenerator: genArrayWithRule("string", "self.all(x, true)"), + expectedCalcCost: 3145727, + setMaxElements: 20, + expectedSetCost: 62, + }, + { + name: "boolean array with all", + schemaGenerator: genArrayWithRule("boolean", "self.all(x, true)"), + expectedCalcCost: 1887437, + setMaxElements: 5, + expectedSetCost: 17, + }, + // all array-of-array tests should have the same expected cost along the same expression, + // since arrays-of-arrays are serialized the same in minimized form regardless of item type + // of the subarray ([[], [], ...]) + { + name: "array of number arrays with all", + schemaGenerator: genArrayOfArraysWithRule("number", "self.all(x, true)"), + expectedCalcCost: 3145727, + setMaxElements: 100, + expectedSetCost: 302, + }, + { + name: "array of objects with all", + schemaGenerator: genObjectArrayWithRule("self.all(x, true)"), + expectedCalcCost: 555128, + setMaxElements: 50, + expectedSetCost: 152, + }, + { + name: "map of numbers with all", + schemaGenerator: genMapWithRule("number", "self.all(x, true)"), + expectedCalcCost: 1348169, + setMaxElements: 10, + expectedSetCost: 32, + }, + { + name: "map of numbers with has", + schemaGenerator: genMapWithRule("number", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 100, + expectedSetCost: 0, + }, + { + name: "map of strings with all", + schemaGenerator: genMapWithRule("string", "self.all(x, true)"), + expectedCalcCost: 1179647, + setMaxElements: 3, + expectedSetCost: 11, + }, + { + name: "map of strings with has", + schemaGenerator: genMapWithRule("string", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 550, + expectedSetCost: 0, + }, + { + name: "map of booleans with all", + schemaGenerator: genMapWithRule("boolean", "self.all(x, true)"), + expectedCalcCost: 943718, + setMaxElements: 100, + expectedSetCost: 302, + }, + { + name: "map of booleans with has", + schemaGenerator: genMapWithRule("boolean", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 1024, + expectedSetCost: 0, + }, + { + name: "string with contains", + schemaGenerator: genStringWithRule("self.contains('test')"), + expectedCalcCost: 314574, + setMaxElements: 10, + expectedSetCost: 5, + }, + { + name: "string with startsWith", + schemaGenerator: genStringWithRule("self.startsWith('test')"), + expectedCalcCost: 2, + setMaxElements: 15, + expectedSetCost: 2, + }, + { + name: "string with endsWith", + schemaGenerator: genStringWithRule("self.endsWith('test')"), + expectedCalcCost: 2, + setMaxElements: 30, + expectedSetCost: 2, + }, + { + name: "concat string", + schemaGenerator: genStringWithRule(`size(self + "hello") > size("hello")`), + expectedCalcCost: 314578, + setMaxElements: 4, + expectedSetCost: 7, + }, + { + name: "index of array with numbers", + schemaGenerator: genArrayWithRule("number", "self[1] == 0.0"), + expectedCalcCost: 2, + setMaxElements: 5000, + expectedSetCost: 2, + }, + { + name: "index of array with strings", + schemaGenerator: genArrayWithRule("string", "self[1] == self[1]"), + expectedCalcCost: 314577, + setMaxElements: 8, + expectedSetCost: 314577, + }, + { + name: "O(n^2) loop with numbers", + schemaGenerator: genArrayWithRule("number", "self.all(x, self.all(y, true))"), + expectedCalcCost: 9895601504256, + expectCalcCostExceedsLimit: costLimit, + setMaxElements: 10, + expectedSetCost: 352, + }, + { + name: "O(n^3) loop with numbers", + schemaGenerator: genArrayWithRule("number", "self.all(x, self.all(y, self.all(z, true)))"), + expectedCalcCost: 13499986500008999998, + expectCalcCostExceedsLimit: costLimit, + setMaxElements: 10, + expectedSetCost: 3552, + }, + { + name: "regex matches simple", + schemaGenerator: genStringWithRule(`self.matches("x")`), + expectedCalcCost: 314574, + setMaxElements: 50, + expectedSetCost: 22, + }, + { + name: "regex matches empty string", + schemaGenerator: genStringWithRule(`"".matches("(((((((((())))))))))[0-9]")`), + expectedCalcCost: 7, + setMaxElements: 10, + expectedSetCost: 7, + }, + { + name: "regex matches empty regex", + schemaGenerator: genStringWithRule(`self.matches("")`), + expectedCalcCost: 1, + setMaxElements: 100, + expectedSetCost: 1, + }, + { + name: "map of strings with value length", + schemaGenerator: genNestedSpecWithRule("self.all(x, x.contains(self[x]))"), + expectedCalcCost: 2752507, + setMaxElements: 10, + expectedSetCost: 72, + }, + { + name: "set array maxLength to zero", + schemaGenerator: genArrayWithRule("number", "self[3] == 0.0"), + expectedCalcCost: 2, + setMaxElements: 0, + expectedSetCost: 2, + }, + { + name: "set map maxLength to zero", + schemaGenerator: genMapWithRule("number", `self["x"] == 0.0`), + expectedCalcCost: 2, + setMaxElements: 0, + expectedSetCost: 2, + }, + { + name: "set string maxLength to zero", + schemaGenerator: genStringWithRule(`self == "x"`), + expectedCalcCost: 2, + setMaxElements: 0, + expectedSetCost: 1, + }, + { + name: "set bytes maxLength to zero", + schemaGenerator: genBytesWithRule(`self == b"x"`), + expectedCalcCost: 2, + setMaxElements: 0, + expectedSetCost: 1, + }, + { + name: "set maxLength greater than estimated maxLength", + schemaGenerator: genArrayWithRule("number", "self.all(x, x == 0.0)"), + expectedCalcCost: 6291454, + setMaxElements: 3 * 1024 * 2048, + expectedSetCost: 25165826, + }, + { + name: "nested types with root rule with all supporting maxLength", + schemaGenerator: genAllMaxNestedSpecWithRootRule(`self.all(x, x["y"].required == "z")`), + expectedCalcCost: 7340027, + setMaxElements: 10, + expectedSetCost: 72, + }, + { + name: "nested types with root rule with one supporting maxLength", + schemaGenerator: genOneMaxNestedSpecWithRootRule(`self.all(x, x["y"].required == "z")`), + expectedCalcCost: 7340027, + setMaxElements: 10, + expectedSetCost: 7340027, + }, + { + name: "int-or-string array with all", + schemaGenerator: genArrayWithRule("int-or-string", "self.all(x, true)"), + expectedCalcCost: 4718591, + setMaxElements: 10, + expectedSetCost: 32, + }, + { + name: "index of array with int-or-strings", + schemaGenerator: genArrayWithRule("int-or-string", "self[0] == 5"), + expectedCalcCost: 3, + setMaxElements: 10, + expectedSetCost: 3, + }, + { + name: "index of array with booleans", + schemaGenerator: genArrayWithRule("boolean", "self[0] == false"), + expectedCalcCost: 2, + setMaxElements: 25, + expectedSetCost: 2, + }, + { + name: "index of array of objects", + schemaGenerator: genObjectArrayWithRule("self[0] == null"), + expectedCalcCost: 2, + setMaxElements: 422, + expectedSetCost: 2, + }, + { + name: "index of array of array of numnbers", + schemaGenerator: genArrayOfArraysWithRule("number", "self[0][0] == -1.0"), + expectedCalcCost: 3, + setMaxElements: 51, + expectedSetCost: 3, + }, + { + name: "array of number maps with all", + schemaGenerator: getMapArrayWithRule("number", `self.all(x, x.y == 25.2)`), + expectedCalcCost: 6291452, + setMaxElements: 12, + expectedSetCost: 74, + }, + { + name: "index of array of number maps", + schemaGenerator: getMapArrayWithRule("number", `self[0].x > 2.0`), + expectedCalcCost: 4, + setMaxElements: 3000, + expectedSetCost: 4, + }, + { + name: "duration array with all", + schemaGenerator: genArrayWithRule("duration", "self.all(x, true)"), + expectedCalcCost: 2359295, + setMaxElements: 5, + expectedSetCost: 17, + }, + { + name: "index of duration array", + schemaGenerator: genArrayWithRule("duration", "self[0].getHours() == 2"), + expectedCalcCost: 4, + setMaxElements: 525, + expectedSetCost: 4, + }, + { + name: "date array with all", + schemaGenerator: genArrayWithRule("date", "self.all(x, true)"), + expectedCalcCost: 725936, + setMaxElements: 15, + expectedSetCost: 47, + }, + { + name: "index of date array", + schemaGenerator: genArrayWithRule("date", "self[2].getDayOfMonth() == 13"), + expectedCalcCost: 4, + setMaxElements: 42, + expectedSetCost: 4, + }, + { + name: "date-time array with all", + schemaGenerator: genArrayWithRule("date-time", "self.all(x, true)"), + expectedCalcCost: 428963, + setMaxElements: 25, + expectedSetCost: 77, + }, + { + name: "index of date-time array", + schemaGenerator: genArrayWithRule("date-time", "self[2].getMinutes() == 45"), + expectedCalcCost: 4, + setMaxElements: 99, + expectedSetCost: 4, + }, + { + name: "map of int-or-strings with all", + schemaGenerator: genMapWithRule("int-or-string", "self.all(x, true)"), + expectedCalcCost: 1348169, + setMaxElements: 15, + expectedSetCost: 47, + }, + { + name: "map of int-or-strings with has", + schemaGenerator: genMapWithRule("int-or-string", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 5000, + expectedSetCost: 0, + }, + { + name: "map of objects with all", + schemaGenerator: genMapWithCustomItemRule(genObjectForMap(), "self.all(x, true)"), + expectedCalcCost: 428963, + setMaxElements: 20, + expectedSetCost: 62, + }, + { + name: "map of objects with has", + schemaGenerator: genMapWithCustomItemRule(genObjectForMap(), "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 9001, + expectedSetCost: 0, + }, + { + name: "map of number maps with all", + schemaGenerator: genMapWithCustomItemRule(genMapForMap(), "self.all(x, true)"), + expectedCalcCost: 1179647, + setMaxElements: 10, + expectedSetCost: 32, + }, + { + name: "map of number maps with has", + schemaGenerator: genMapWithCustomItemRule(genMapForMap(), "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 101, + expectedSetCost: 0, + }, + { + name: "map of number arrays with all", + schemaGenerator: genMapWithCustomItemRule(genArrayForMap(), "self.all(x, true)"), + expectedCalcCost: 1179647, + setMaxElements: 25, + expectedSetCost: 77, + }, + { + name: "map of number arrays with has", + schemaGenerator: genMapWithCustomItemRule(genArrayForMap(), "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 40000, + expectedSetCost: 0, + }, + { + name: "map of durations with all", + schemaGenerator: genMapWithRule("duration", "self.all(x, true)"), + expectedCalcCost: 1048577, + setMaxElements: 5, + expectedSetCost: 17, + }, + { + name: "map of durations with has", + schemaGenerator: genMapWithRule("duration", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 256, + expectedSetCost: 0, + }, + { + name: "map of dates with all", + schemaGenerator: genMapWithRule("date", "self.all(x, true)"), + expectedCalcCost: 524288, + setMaxElements: 10, + expectedSetCost: 32, + }, + { + name: "map of dates with has", + schemaGenerator: genMapWithRule("date", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 65536, + expectedSetCost: 0, + }, + { + name: "map of date-times with all", + schemaGenerator: genMapWithRule("date-time", "self.all(x, true)"), + expectedCalcCost: 349526, + setMaxElements: 25, + expectedSetCost: 77, + }, + { + name: "map of date-times with has", + schemaGenerator: genMapWithRule("date-time", "has(self.x)"), + expectedCalcCost: 0, + setMaxElements: 490, + expectedSetCost: 0, + }, + } + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + // dynamic maxLength case + schema := testCase.schemaGenerator(nil) + t.Run("calc maxLength", schemaChecker(schema, testCase.expectedCalcCost, testCase.expectCalcCostExceedsLimit, t)) + // static maxLength case + setSchema := testCase.schemaGenerator(&testCase.setMaxElements) + t.Run("set maxLength", schemaChecker(setSchema, testCase.expectedSetCost, 0, t)) + }) + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go index 6a22eab2bfb..382f97fc440 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation_test.go @@ -1682,12 +1682,17 @@ func TestValidationExpressions(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for i := range tests { + i := i + t.Run(tests[i].name, func(t *testing.T) { + t.Parallel() // set costBudget to maxInt64 for current test + tt := tests[i] tt.costBudget = math.MaxInt64 - for _, validRule := range tt.valid { + for j := range tt.valid { + validRule := tt.valid[j] t.Run(validRule, func(t *testing.T) { + t.Parallel() s := withRule(*tt.schema, validRule) celValidator := NewValidator(&s, PerCallLimit) if celValidator == nil { diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go index 7d8e910da0c..b0ada9b1163 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go @@ -15,9 +15,36 @@ package model import ( + "time" + + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" ) +const ( + // the largest request that will be accepted is 3MB + // TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable + maxRequestSizeBytes = int64(3 * 1024 * 1024) + // OpenAPI duration strings follow RFC 3339, section 5.6 - see the comment on maxDatetimeSizeJSON + maxDurationSizeJSON = 32 + // OpenAPI datetime strings follow RFC 3339, section 5.6, and the longest possible + // such string is 9999-12-31T23:59:59.999999999Z, which has length 30 - we add 2 + // to allow for quotation marks + maxDatetimeSizeJSON = 32 + // Golang allows a string of 0 to be parsed as a duration, so that plus 2 to account for + // quotation marks makes 3 + minDurationSizeJSON = 3 + // RFC 3339 dates require YYYY-MM-DD, and then we add 2 to allow for quotation marks + dateSizeJSON = 12 + // RFC 3339 times require 2-digit 24-hour time at the very least plus a capital T at the start, + // e.g., T23, and we add 2 to allow for quotation marks as usual + minTimeSizeJSON = 5 + // RFC 3339 datetimes require a full date (YYYY-MM-DD) and full time (HH:MM:SS), and we add 3 for + // quotation marks like always in addition to the capital T that separates the date and time + minDatetimeSizeJSON = 21 +) + // SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the // the structural schema should not be exposed in CEL expressions. // Set isResourceRoot to true for the root of a custom resource or embedded resource. @@ -44,7 +71,10 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType { // To validate requirements on both the int and string representation: // `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5 // - return DynType + dyn := newSimpleType("dyn", decls.Dyn, nil) + // handle x-kubernetes-int-or-string by returning the max length of the largest possible string + dyn.MaxElements = maxRequestSizeBytes - 2 + return dyn } // We ignore XPreserveUnknownFields since we don't support validation rules on @@ -61,8 +91,14 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType { case "array": if s.Items != nil { itemsType := SchemaDeclType(s.Items, s.Items.XEmbeddedResource) + var maxItems int64 + if s.ValueValidation != nil && s.ValueValidation.MaxItems != nil { + maxItems = *s.ValueValidation.MaxItems + } else { + maxItems = estimateMaxArrayItemsPerRequest(s.Items) + } if itemsType != nil { - return NewListType(itemsType) + return NewListType(itemsType, maxItems) } } return nil @@ -70,7 +106,13 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType { if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil { propsType := SchemaDeclType(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource) if propsType != nil { - return NewMapType(StringType, propsType) + var maxProperties int64 + if s.ValueValidation != nil && s.ValueValidation.MaxProperties != nil { + maxProperties = *s.ValueValidation.MaxProperties + } else { + maxProperties = estimateMaxAdditionalPropertiesPerRequest(s.AdditionalProperties.Structural) + } + return NewMapType(StringType, propsType, maxProperties) } return nil } @@ -106,14 +148,34 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType { if s.ValueValidation != nil { switch s.ValueValidation.Format { case "byte": - return BytesType + byteWithMaxLength := newSimpleType("bytes", decls.Bytes, types.Bytes([]byte{})) + if s.ValueValidation.MaxLength != nil { + byteWithMaxLength.MaxElements = *s.ValueValidation.MaxLength + } else { + byteWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s) + } + return byteWithMaxLength case "duration": - return DurationType + durationWithMaxLength := newSimpleType("duration", decls.Duration, types.Duration{Duration: time.Duration(0)}) + durationWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s) + return durationWithMaxLength case "date", "date-time": - return TimestampType + timestampWithMaxLength := newSimpleType("timestamp", decls.Timestamp, types.Timestamp{Time: time.Time{}}) + timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s) + return timestampWithMaxLength } } - return StringType + strWithMaxLength := newSimpleType("string", decls.String, types.String("")) + if s.ValueValidation != nil && s.ValueValidation.MaxLength != nil { + // multiply the user-provided max length by 4 in the case of an otherwise-untyped string + // we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points, + // but we need to reason about length for things like request size, so we use bytes in this code (and an individual + // unicode code point can be up to 4 bytes long) + strWithMaxLength.MaxElements = *s.ValueValidation.MaxLength * 4 + } else { + strWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s) + } + return strWithMaxLength case "boolean": return BoolType case "number": @@ -137,8 +199,8 @@ func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural { return s } result := &schema.Structural{ - Generic: s.Generic, - Extensions: s.Extensions, + Generic: s.Generic, + Extensions: s.Extensions, ValueValidation: s.ValueValidation, } props := make(map[string]schema.Structural, len(s.Properties)) @@ -151,11 +213,107 @@ func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural { props["metadata"] = schema.Structural{ Generic: schema.Generic{Type: "object"}, Properties: map[string]schema.Structural{ - "name": stringType, + "name": stringType, "generateName": stringType, }, } result.Properties = props return result -} \ No newline at end of file +} + +// estimateMinSizeJSON estimates the minimum size in bytes of the given schema when serialized in JSON. +// minLength/minProperties/minItems are not currently taken into account, so if these limits are set the +// minimum size might be higher than what estimateMinSizeJSON returns. +func estimateMinSizeJSON(s *schema.Structural) int64 { + if s == nil { + // minimum valid JSON token has length 1 (single-digit number like `0`) + return 1 + } + switch s.Type { + case "boolean": + // true + return 4 + case "number", "integer": + // 0 + return 1 + case "string": + if s.ValueValidation != nil { + switch s.ValueValidation.Format { + case "duration": + return minDurationSizeJSON + case "date": + return dateSizeJSON + case "date-time": + return minDatetimeSizeJSON + } + } + // "" + return 2 + case "array": + // [] + return 2 + case "object": + // {} + objSize := int64(2) + // exclude optional fields since the request can omit them + if s.ValueValidation != nil { + for _, propName := range s.ValueValidation.Required { + if prop, ok := s.Properties[propName]; ok { + if prop.Default.Object != nil { + // exclude fields with a default, those are filled in server-side + continue + } + // add 4, 2 for quotations around the property name, 1 for the colon, and 1 for a comma + objSize += int64(len(propName)) + estimateMinSizeJSON(&prop) + 4 + } + } + } + return objSize + } + if s.XIntOrString { + // 0 + return 1 + } + // this code should be unreachable, so return the safest possible value considering this can be used as + // a divisor + return 1 +} + +// estimateMaxArrayItemsPerRequest estimates the maximum number of array items with +// the provided schema that can fit into a single request. +func estimateMaxArrayItemsPerRequest(itemSchema *schema.Structural) int64 { + // subtract 2 to account for [ and ] + return (maxRequestSizeBytes - 2) / (estimateMinSizeJSON(itemSchema) + 1) +} + +// estimateMaxStringLengthPerRequest estimates the maximum string length (in characters) +// of a string compatible with the format requirements in the provided schema. +// must only be called on schemas of type "string" or x-kubernetes-int-or-string: true +func estimateMaxStringLengthPerRequest(s *schema.Structural) int64 { + if s.ValueValidation == nil || s.XIntOrString { + // subtract 2 to account for "" + return (maxRequestSizeBytes - 2) + } + switch s.ValueValidation.Format { + case "duration": + return maxDurationSizeJSON + case "date": + return dateSizeJSON + case "date-time": + return maxDatetimeSizeJSON + default: + // subtract 2 to account for "" + return (maxRequestSizeBytes - 2) + } +} + +// estimateMaxAdditionalPropertiesPerRequest estimates the maximum number of additional properties +// with the provided schema that can fit into a single request. +func estimateMaxAdditionalPropertiesPerRequest(additionalPropertiesSchema *schema.Structural) int64 { + // 2 bytes for key + "" + colon + comma + smallest possible value, realistically the actual keys + // will all vary in length + keyValuePairSize := estimateMinSizeJSON(additionalPropertiesSchema) + 6 + // subtract 2 to account for { and } + return (maxRequestSizeBytes - 2) / keyValuePairSize +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go index 37b7705597d..2490911a785 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/schemas_test.go @@ -238,3 +238,267 @@ func testSchema() *schema.Structural { } return ts } + +func arraySchema(arrayType, format string, maxItems *int64) *schema.Structural { + return &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: arrayType, + }, + ValueValidation: &schema.ValueValidation{ + Format: format, + }, + }, + ValueValidation: &schema.ValueValidation{ + MaxItems: maxItems, + }, + } +} + +func TestEstimateMaxLengthJSON(t *testing.T) { + type maxLengthTest struct { + Name string + InputSchema *schema.Structural + ExpectedMaxElements int64 + } + tests := []maxLengthTest{ + { + Name: "booleanArray", + InputSchema: arraySchema("boolean", "", nil), + // expected JSON is [true,true,...], so our length should be (maxRequestSizeBytes - 2) / 5 + ExpectedMaxElements: 629145, + }, + { + Name: "durationArray", + InputSchema: arraySchema("string", "duration", nil), + // expected JSON is ["0","0",...] so our length should be (maxRequestSizeBytes - 2) / 4 + ExpectedMaxElements: 786431, + }, + { + Name: "datetimeArray", + InputSchema: arraySchema("string", "date-time", nil), + // expected JSON is ["2000-01-01T01:01:01","2000-01-01T01:01:01",...] so our length should be (maxRequestSizeBytes - 2) / 22 + ExpectedMaxElements: 142987, + }, + { + Name: "dateArray", + InputSchema: arraySchema("string", "date", nil), + // expected JSON is ["2000-01-01","2000-01-02",...] so our length should be (maxRequestSizeBytes - 2) / 13 + ExpectedMaxElements: 241978, + }, + { + Name: "numberArray", + InputSchema: arraySchema("integer", "", nil), + // expected JSON is [0,0,...] so our length should be (maxRequestSizeBytes - 2) / 2 + ExpectedMaxElements: 1572863, + }, + { + Name: "stringArray", + InputSchema: arraySchema("string", "", nil), + // expected JSON is ["","",...] so our length should be (maxRequestSizeBytes - 2) / 3 + ExpectedMaxElements: 1048575, + }, + { + Name: "stringMap", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + }}, + }, + }, + // expected JSON is {"":"","":"",...} so our length should be (3000000 - 2) / 6 + ExpectedMaxElements: 393215, + }, + { + Name: "objectOptionalPropertyArray", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "required": schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + }, + "optional": schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + Required: []string{"required"}, + }, + }, + }, + // expected JSON is [{"required":"",},{"required":"",},...] so our length should be (maxRequestSizeBytes - 2) / 17 + ExpectedMaxElements: 185042, + }, + { + Name: "arrayWithLength", + InputSchema: arraySchema("integer", "int64", maxPtr(10)), + // manually set by MaxItems + ExpectedMaxElements: 10, + }, + { + Name: "stringWithLength", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + MaxLength: maxPtr(20), + }, + }, + // manually set by MaxLength, but we expect a 4x multiplier compared to the original input + // since OpenAPIv3 maxLength uses code points, but DeclType works with bytes + ExpectedMaxElements: 80, + }, + { + Name: "mapWithLength", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + }}, + }, + ValueValidation: &schema.ValueValidation{ + Format: "string", + MaxProperties: maxPtr(15), + }, + }, + // manually set by MaxProperties + ExpectedMaxElements: 15, + }, + { + Name: "durationMaxSize", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + Format: "duration", + }, + }, + // should be exactly equal to maxDurationSizeJSON + ExpectedMaxElements: maxDurationSizeJSON, + }, + { + Name: "dateSize", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + Format: "date", + }, + }, + // should be exactly equal to dateSizeJSON + ExpectedMaxElements: dateSizeJSON, + }, + { + Name: "maxdatetimeSize", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + Format: "date-time", + }, + }, + // should be exactly equal to maxDatetimeSizeJSON + ExpectedMaxElements: maxDatetimeSizeJSON, + }, + { + Name: "maxintOrStringSize", + InputSchema: &schema.Structural{ + Extensions: schema.Extensions{ + XIntOrString: true, + }, + }, + // should be exactly equal to maxRequestSizeBytes - 2 (to allow for quotes in the case of a string) + ExpectedMaxElements: maxRequestSizeBytes - 2, + }, + { + Name: "objectDefaultFieldArray", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "array", + }, + Items: &schema.Structural{ + Generic: schema.Generic{ + Type: "object", + }, + Properties: map[string]schema.Structural{ + "field": schema.Structural{ + Generic: schema.Generic{ + Type: "string", + Default: schema.JSON{Object: "default"}, + }, + }, + }, + ValueValidation: &schema.ValueValidation{ + Required: []string{"field"}, + }, + }, + }, + // expected JSON is [{},{},...] so our length should be (maxRequestSizeBytes - 2) / 3 + ExpectedMaxElements: 1048575, + }, + { + Name: "byteStringSize", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + Format: "byte", + }, + }, + // expected JSON is "" so our length should be (maxRequestSizeBytes - 2) + ExpectedMaxElements: 3145726, + }, + { + Name: "byteStringSetMaxLength", + InputSchema: &schema.Structural{ + Generic: schema.Generic{ + Type: "string", + }, + ValueValidation: &schema.ValueValidation{ + Format: "byte", + MaxLength: maxPtr(20), + }, + }, + // note that unlike regular strings we don't have to take unicode into account, + // so we we expect the max length to be exactly equal to the user-supplied one + ExpectedMaxElements: 20, + }, + } + for _, testCase := range tests { + t.Run(testCase.Name, func(t *testing.T) { + decl := SchemaDeclType(testCase.InputSchema, false) + if decl.MaxElements != testCase.ExpectedMaxElements { + t.Errorf("wrong maxElements (got %d, expected %d)", decl.MaxElements, testCase.ExpectedMaxElements) + } + }) + } +} + +func maxPtr(max int64) *int64 { + return &max +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go index 0822e1d7477..2f0a5b59dcd 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types.go @@ -16,6 +16,7 @@ package model import ( "fmt" + "math" "time" "github.com/google/cel-go/cel" @@ -30,22 +31,28 @@ import ( "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" ) +const ( + noMaxLength = math.MaxInt +) + // NewListType returns a parameterized list type with a specified element type. -func NewListType(elem *DeclType) *DeclType { +func NewListType(elem *DeclType, maxItems int64) *DeclType { return &DeclType{ name: "list", ElemType: elem, + MaxElements: maxItems, exprType: decls.NewListType(elem.ExprType()), defaultValue: NewListValue(), } } // NewMapType returns a parameterized map type with the given key and element types. -func NewMapType(key, elem *DeclType) *DeclType { +func NewMapType(key, elem *DeclType, maxProperties int64) *DeclType { return &DeclType{ name: "map", KeyType: key, ElemType: elem, + MaxElements: maxProperties, exprType: decls.NewMapType(key.ExprType(), elem.ExprType()), defaultValue: NewMapValue(), } @@ -97,12 +104,13 @@ func newSimpleType(name string, exprType *exprpb.Type, zeroVal ref.Val) *DeclTyp type DeclType struct { fmt.Stringer - name string - Fields map[string]*DeclField - KeyType *DeclType - ElemType *DeclType - TypeParam bool - Metadata map[string]string + name string + Fields map[string]*DeclField + KeyType *DeclType + ElemType *DeclType + TypeParam bool + Metadata map[string]string + MaxElements int64 exprType *exprpb.Type traitMask int @@ -160,7 +168,7 @@ func (t *DeclType) MaybeAssignTypeName(name string) *DeclType { if updated == t.ElemType { return t } - return NewMapType(t.KeyType, updated) + return NewMapType(t.KeyType, updated, t.MaxElements) } if t.IsList() { elemTypeName := fmt.Sprintf("%s.@idx", name) @@ -168,7 +176,7 @@ func (t *DeclType) MaybeAssignTypeName(name string) *DeclType { if updated == t.ElemType { return t } - return NewListType(updated) + return NewListType(updated, t.MaxElements) } return t } @@ -547,8 +555,8 @@ var ( UintType = newSimpleType("uint", decls.Uint, types.Uint(0)) // ListType is equivalent to the CEL 'list' type. - ListType = NewListType(AnyType) + ListType = NewListType(AnyType, noMaxLength) // MapType is equivalent to the CEL 'map' type. - MapType = NewMapType(AnyType, AnyType) + MapType = NewMapType(AnyType, AnyType, noMaxLength) ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go index b4a8da8dfa2..66756bfda95 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model/types_test.go @@ -24,7 +24,7 @@ import ( ) func TestTypes_ListType(t *testing.T) { - list := NewListType(StringType) + list := NewListType(StringType, -1) if !list.IsList() { t.Error("list type not identifiable as list") } @@ -43,7 +43,7 @@ func TestTypes_ListType(t *testing.T) { } func TestTypes_MapType(t *testing.T) { - mp := NewMapType(StringType, IntType) + mp := NewMapType(StringType, IntType, -1) if !mp.IsMap() { t.Error("map type not identifiable as map") } diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index 375c4dc24c5..c16407606f3 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -359,6 +359,9 @@ func NewConfig(codecs serializer.CodecFactory) *Config { // A request body might be encoded in json, and is converted to // proto when persisted in etcd, so we allow 2x as the largest request // body size to be accepted and decoded in a write request. + // If this constant is changed, maxRequestSizeBytes in apiextensions-apiserver/third_party/forked/celopenapi/model/schemas.go + // should be changed to reflect the new value, if the two haven't + // been wired together already somehow. MaxRequestBodyBytes: int64(3 * 1024 * 1024), // Default to treating watch as a long-running operation