Skip to content

Commit 4ad924f

Browse files
Merge pull request #2375 from JoelSpeed/fix-max-elements-x-int-or-string-release-4.20
OCPBUGS-59756: Fix IntOrString cost estimation when schema has a MaxLength constraint
2 parents 24677bb + 862dc90 commit 4ad924f

File tree

3 files changed

+72
-9
lines changed

3 files changed

+72
-9
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,6 +1872,18 @@ func TestCostEstimation(t *testing.T) {
18721872
setMaxElements: 1000,
18731873
expectedSetCost: 401,
18741874
},
1875+
{
1876+
name: "IntOrString type with quantity rule",
1877+
schemaGenerator: func(max *int64) *schema.Structural {
1878+
intOrString := intOrStringType()
1879+
intOrString = withRule(intOrString, "isQuantity(self)")
1880+
intOrString = withMaxLength(intOrString, max)
1881+
return &intOrString
1882+
},
1883+
expectedCalcCost: 314574,
1884+
setMaxElements: 20,
1885+
expectedSetCost: 9,
1886+
},
18751887
}
18761888
for _, testCase := range cases {
18771889
t.Run(testCase.name, func(t *testing.T) {

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas_test.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ func TestSchemaDeclType(t *testing.T) {
3434
if cust.TypeName() != "object" {
3535
t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName())
3636
}
37-
if len(cust.Fields) != 4 {
38-
t.Errorf("incorrect number of fields, got %d, wanted 4", len(cust.Fields))
37+
if len(cust.Fields) != 5 {
38+
t.Errorf("incorrect number of fields, got %d, wanted 5", len(cust.Fields))
3939
}
4040
for _, f := range cust.Fields {
4141
prop, found := ts.Properties[f.Name]
@@ -70,6 +70,13 @@ func TestSchemaDeclType(t *testing.T) {
7070
}
7171
}
7272
}
73+
if prop.ValueValidation != nil && prop.ValueValidation.MaxLength != nil {
74+
if f.Type.MaxElements != 4*(*prop.ValueValidation.MaxLength) {
75+
// When converting maxLength to maxElements, it's based on the number of bytes.]
76+
// Worst case is that one rune is 4 bytes, so maxElements should be 4x maxLength.
77+
t.Errorf("field maxElements does not match property 4x maxLength. field: %s, maxElements: %d, maxLength: %d", f.Name, f.Type.MaxElements, *prop.ValueValidation.MaxLength)
78+
}
79+
}
7380
}
7481
if ts.ValueValidation != nil {
7582
for _, name := range ts.ValueValidation.Required {
@@ -137,6 +144,7 @@ func testSchema() *schema.Structural {
137144
// properties:
138145
// name:
139146
// type: string
147+
// maxLength: 256
140148
// nested:
141149
// type: object
142150
// properties:
@@ -166,6 +174,12 @@ func testSchema() *schema.Structural {
166174
// format: int64
167175
// default: 1
168176
// enum: [1,2,3]
177+
// intOrString:
178+
// x-kubernetes-int-or-string: true
179+
// anyOf:
180+
// - type: "integer"
181+
// - type: "string"
182+
// maxLength: 20
169183
ts := &schema.Structural{
170184
Generic: schema.Generic{
171185
Type: "object",
@@ -175,6 +189,9 @@ func testSchema() *schema.Structural {
175189
Generic: schema.Generic{
176190
Type: "string",
177191
},
192+
ValueValidation: &schema.ValueValidation{
193+
MaxLength: maxPtr(256),
194+
},
178195
},
179196
"value": {
180197
Generic: schema.Generic{
@@ -245,6 +262,26 @@ func testSchema() *schema.Structural {
245262
},
246263
},
247264
},
265+
"intOrString": {
266+
Extensions: schema.Extensions{
267+
XIntOrString: true,
268+
},
269+
ValueValidation: &schema.ValueValidation{
270+
MaxLength: maxPtr(20),
271+
AnyOf: []schema.NestedValueValidation{
272+
{
273+
ForbiddenGenerics: schema.Generic{
274+
Type: "integer",
275+
},
276+
},
277+
{
278+
ForbiddenGenerics: schema.Generic{
279+
Type: "string",
280+
},
281+
},
282+
},
283+
},
284+
},
248285
},
249286
}
250287
return ts

staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,15 @@ func SchemaDeclType(s Schema, isResourceRoot bool) *apiservercel.DeclType {
5555
// `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
5656
//
5757
dyn := apiservercel.NewSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1) // smallest value for a serialized x-kubernetes-int-or-string is 0
58-
// handle x-kubernetes-int-or-string by returning the max length/min serialized size of the largest possible string
59-
dyn.MaxElements = maxRequestSizeBytes - 2
58+
59+
// If the schema has a maxlength constraint, bound the max elements based on the max length.
60+
// Otherwise, fallback to the max request size.
61+
if s.MaxLength() != nil {
62+
dyn.MaxElements = estimateMaxElementsFromMaxLength(s)
63+
} else {
64+
dyn.MaxElements = estimateMaxStringLengthPerRequest(s)
65+
}
66+
6067
return dyn
6168
}
6269

@@ -159,11 +166,7 @@ func SchemaDeclType(s Schema, isResourceRoot bool) *apiservercel.DeclType {
159166

160167
strWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), apiservercel.MinStringSize)
161168
if s.MaxLength() != nil {
162-
// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
163-
// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
164-
// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
165-
// unicode code point can be up to 4 bytes long)
166-
strWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength()) * 4
169+
strWithMaxLength.MaxElements = estimateMaxElementsFromMaxLength(s)
167170
} else {
168171
if len(s.Enum()) > 0 {
169172
strWithMaxLength.MaxElements = estimateMaxStringEnumLength(s)
@@ -228,6 +231,7 @@ func WithTypeAndObjectMeta(s *spec.Schema) *spec.Schema {
228231
// must only be called on schemas of type "string" or x-kubernetes-int-or-string: true
229232
func estimateMaxStringLengthPerRequest(s Schema) int64 {
230233
if s.IsXIntOrString() {
234+
// handle x-kubernetes-int-or-string by returning the max length/min serialized size of the largest possible string
231235
return maxRequestSizeBytes - 2
232236
}
233237
switch s.Format() {
@@ -272,3 +276,13 @@ func estimateMaxAdditionalPropertiesFromMinSize(minSize int64) int64 {
272276
// subtract 2 to account for { and }
273277
return (maxRequestSizeBytes - 2) / keyValuePairSize
274278
}
279+
280+
// estimateMaxElementsFromMaxLength estimates the maximum number of elements for a string schema
281+
// that is bound with a maxLength constraint.
282+
func estimateMaxElementsFromMaxLength(s Schema) int64 {
283+
// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
284+
// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
285+
// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
286+
// unicode code point can be up to 4 bytes long)
287+
return zeroIfNegative(*s.MaxLength()) * 4
288+
}

0 commit comments

Comments
 (0)