mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 11:21:47 +00:00
Merge pull request #108419 from DangerOnTheRanger/cel-maxlength-integration
CEL MaxLength integration
This commit is contained in:
commit
1d7599b56c
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user