k8s.io/apiserver@v0.31.1/pkg/cel/openapi/schemas_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package openapi 18 19 import ( 20 "reflect" 21 "testing" 22 23 "github.com/google/cel-go/common/types" 24 25 "google.golang.org/protobuf/proto" 26 27 apiservercel "k8s.io/apiserver/pkg/cel" 28 "k8s.io/kube-openapi/pkg/validation/spec" 29 ) 30 31 func TestSchemaDeclType(t *testing.T) { 32 ts := testSchema() 33 cust := SchemaDeclType(ts, false) 34 if cust.TypeName() != "object" { 35 t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName()) 36 } 37 if len(cust.Fields) != 4 { 38 t.Errorf("incorrect number of fields, got %d, wanted 4", len(cust.Fields)) 39 } 40 for _, f := range cust.Fields { 41 prop, found := ts.Properties[f.Name] 42 if !found { 43 t.Errorf("type field not found in schema, field: %s", f.Name) 44 } 45 fdv := f.DefaultValue() 46 if prop.Default != nil { 47 pdv := types.DefaultTypeAdapter.NativeToValue(prop.Default) 48 if !reflect.DeepEqual(fdv, pdv) { 49 t.Errorf("field and schema do not agree on default value for field: %s, field value: %v, schema default: %v", f.Name, fdv, pdv) 50 } 51 } 52 if (len(prop.Enum) == 0) && len(f.EnumValues()) != 0 { 53 t.Errorf("field had more enum values than the property. field: %s", f.Name) 54 } 55 56 fevs := f.EnumValues() 57 for _, fev := range fevs { 58 found := false 59 for _, pev := range prop.Enum { 60 celpev := types.DefaultTypeAdapter.NativeToValue(pev) 61 if reflect.DeepEqual(fev, celpev) { 62 found = true 63 break 64 } 65 } 66 if !found { 67 t.Errorf( 68 "could not find field enum value in property definition. field: %s, enum: %v", 69 f.Name, fev) 70 } 71 } 72 73 } 74 for _, name := range ts.Required { 75 df, found := cust.FindField(name) 76 if !found { 77 t.Errorf("custom type missing required field. field=%s", name) 78 } 79 if !df.Required { 80 t.Errorf("field marked as required in schema, but optional in type. field=%s", df.Name) 81 } 82 } 83 84 } 85 86 func TestSchemaDeclTypes(t *testing.T) { 87 ts := testSchema() 88 cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject") 89 typeMap := apiservercel.FieldTypeMap("CustomObject", cust) 90 nested, _ := cust.FindField("nested") 91 metadata, _ := cust.FindField("metadata") 92 expectedObjTypeMap := map[string]*apiservercel.DeclType{ 93 "CustomObject": cust, 94 "CustomObject.nested": nested.Type, 95 "CustomObject.metadata": metadata.Type, 96 } 97 objTypeMap := map[string]*apiservercel.DeclType{} 98 for name, t := range typeMap { 99 if t.IsObject() { 100 objTypeMap[name] = t 101 } 102 } 103 if len(objTypeMap) != len(expectedObjTypeMap) { 104 t.Errorf("got different type set. got=%v, wanted=%v", objTypeMap, expectedObjTypeMap) 105 } 106 for exp, expType := range expectedObjTypeMap { 107 actType, found := objTypeMap[exp] 108 if !found { 109 t.Errorf("missing type in rule types: %s", exp) 110 continue 111 } 112 expT, err := expType.ExprType() 113 if err != nil { 114 t.Errorf("fail to get cel type: %s", err) 115 } 116 actT, err := actType.ExprType() 117 if err != nil { 118 t.Errorf("fail to get cel type: %s", err) 119 } 120 if !proto.Equal(expT, actT) { 121 t.Errorf("incompatible CEL types. got=%v, wanted=%v", expT, actT) 122 } 123 } 124 } 125 126 func testSchema() *spec.Schema { 127 // Manual construction of a schema with the following definition: 128 // 129 // schema: 130 // type: object 131 // metadata: 132 // custom_type: "CustomObject" 133 // required: 134 // - name 135 // - value 136 // properties: 137 // name: 138 // type: string 139 // nested: 140 // type: object 141 // properties: 142 // subname: 143 // type: string 144 // flags: 145 // type: object 146 // additionalProperties: 147 // type: boolean 148 // dates: 149 // type: array 150 // items: 151 // type: string 152 // format: date-time 153 // metadata: 154 // type: object 155 // additionalProperties: 156 // type: object 157 // properties: 158 // key: 159 // type: string 160 // values: 161 // type: array 162 // items: string 163 // value: 164 // type: integer 165 // format: int64 166 // default: 1 167 // enum: [1,2,3] 168 ts := &spec.Schema{ 169 SchemaProps: spec.SchemaProps{ 170 Type: []string{"object"}, 171 Properties: map[string]spec.Schema{ 172 "name": *spec.StringProperty(), 173 "value": {SchemaProps: spec.SchemaProps{ 174 Type: []string{"integer"}, 175 Default: int64(1), 176 Format: "int64", 177 Enum: []any{1, 2, 3}, 178 }}, 179 "nested": {SchemaProps: spec.SchemaProps{ 180 Type: []string{"object"}, 181 Properties: map[string]spec.Schema{ 182 "subname": *spec.StringProperty(), 183 "flags": {SchemaProps: spec.SchemaProps{ 184 Type: []string{"object"}, 185 AdditionalProperties: &spec.SchemaOrBool{ 186 Schema: spec.BooleanProperty(), 187 }, 188 }}, 189 "dates": {SchemaProps: spec.SchemaProps{ 190 Type: []string{"array"}, 191 Items: &spec.SchemaOrArray{Schema: &spec.Schema{ 192 SchemaProps: spec.SchemaProps{ 193 Type: []string{"string"}, 194 Format: "date-time", 195 }}}}}, 196 }, 197 }, 198 }, 199 "metadata": {SchemaProps: spec.SchemaProps{ 200 Type: []string{"object"}, 201 Properties: map[string]spec.Schema{ 202 "name": *spec.StringProperty(), 203 "value": { 204 SchemaProps: spec.SchemaProps{ 205 Type: []string{"array"}, 206 Items: &spec.SchemaOrArray{Schema: &spec.Schema{ 207 SchemaProps: spec.SchemaProps{ 208 Type: []string{"string"}, 209 }}}, 210 }, 211 }, 212 }, 213 }}, 214 }}} 215 return ts 216 } 217 218 func arraySchema(arrayType, format string, maxItems *int64) *spec.Schema { 219 return &spec.Schema{ 220 SchemaProps: spec.SchemaProps{ 221 Type: []string{"array"}, 222 Items: &spec.SchemaOrArray{Schema: &spec.Schema{ 223 SchemaProps: spec.SchemaProps{ 224 Type: []string{arrayType}, 225 Format: format, 226 }}}, 227 MaxItems: maxItems, 228 }, 229 } 230 } 231 232 func maxPtr(max int64) *int64 { 233 return &max 234 } 235 236 func TestEstimateMaxLengthJSON(t *testing.T) { 237 type maxLengthTest struct { 238 Name string 239 InputSchema *spec.Schema 240 ExpectedMaxElements int64 241 } 242 tests := []maxLengthTest{ 243 { 244 Name: "booleanArray", 245 InputSchema: arraySchema("boolean", "", nil), 246 // expected JSON is [true,true,...], so our length should be (maxRequestSizeBytes - 2) / 5 247 ExpectedMaxElements: 629145, 248 }, 249 { 250 Name: "durationArray", 251 InputSchema: arraySchema("string", "duration", nil), 252 // expected JSON is ["0","0",...] so our length should be (maxRequestSizeBytes - 2) / 4 253 ExpectedMaxElements: 786431, 254 }, 255 { 256 Name: "datetimeArray", 257 InputSchema: arraySchema("string", "date-time", nil), 258 // expected JSON is ["2000-01-01T01:01:01","2000-01-01T01:01:01",...] so our length should be (maxRequestSizeBytes - 2) / 22 259 ExpectedMaxElements: 142987, 260 }, 261 { 262 Name: "dateArray", 263 InputSchema: arraySchema("string", "date", nil), 264 // expected JSON is ["2000-01-01","2000-01-02",...] so our length should be (maxRequestSizeBytes - 2) / 13 265 ExpectedMaxElements: 241978, 266 }, 267 { 268 Name: "numberArray", 269 InputSchema: arraySchema("integer", "", nil), 270 // expected JSON is [0,0,...] so our length should be (maxRequestSizeBytes - 2) / 2 271 ExpectedMaxElements: 1572863, 272 }, 273 { 274 Name: "stringArray", 275 InputSchema: arraySchema("string", "", nil), 276 // expected JSON is ["","",...] so our length should be (maxRequestSizeBytes - 2) / 3 277 ExpectedMaxElements: 1048575, 278 }, 279 { 280 Name: "stringMap", 281 InputSchema: &spec.Schema{ 282 SchemaProps: spec.SchemaProps{ 283 Type: []string{"object"}, 284 AdditionalProperties: &spec.SchemaOrBool{ 285 Schema: &spec.Schema{ 286 SchemaProps: spec.SchemaProps{ 287 Type: []string{"string"}, 288 }}, 289 }, 290 }}, 291 // expected JSON is {"":"","":"",...} so our length should be (3000000 - 2) / 6 292 ExpectedMaxElements: 393215, 293 }, 294 { 295 Name: "objectOptionalPropertyArray", 296 InputSchema: &spec.Schema{ 297 SchemaProps: spec.SchemaProps{ 298 Type: []string{"array"}, 299 Items: &spec.SchemaOrArray{Schema: &spec.Schema{ 300 SchemaProps: spec.SchemaProps{ 301 Type: []string{"object"}, 302 Properties: map[string]spec.Schema{ 303 "required": *spec.StringProperty(), 304 "optional": *spec.StringProperty(), 305 }, 306 Required: []string{"required"}, 307 }}}, 308 }}, 309 // expected JSON is [{"required":"",},{"required":"",},...] so our length should be (maxRequestSizeBytes - 2) / 17 310 ExpectedMaxElements: 185042, 311 }, 312 { 313 Name: "arrayWithLength", 314 InputSchema: arraySchema("integer", "int64", maxPtr(10)), 315 // manually set by MaxItems 316 ExpectedMaxElements: 10, 317 }, 318 { 319 Name: "stringWithLength", 320 InputSchema: &spec.Schema{ 321 SchemaProps: spec.SchemaProps{ 322 Type: []string{"string"}, 323 MaxLength: maxPtr(20), 324 }}, 325 // manually set by MaxLength, but we expect a 4x multiplier compared to the original input 326 // since OpenAPIv3 maxLength uses code points, but DeclType works with bytes 327 ExpectedMaxElements: 80, 328 }, 329 { 330 Name: "mapWithLength", 331 InputSchema: &spec.Schema{ 332 SchemaProps: spec.SchemaProps{ 333 Type: []string{"object"}, 334 AdditionalProperties: &spec.SchemaOrBool{ 335 Schema: spec.StringProperty(), 336 }, 337 Format: "string", 338 MaxProperties: maxPtr(15), 339 }}, 340 // manually set by MaxProperties 341 ExpectedMaxElements: 15, 342 }, 343 { 344 Name: "durationMaxSize", 345 InputSchema: &spec.Schema{ 346 SchemaProps: spec.SchemaProps{ 347 Type: []string{"string"}, 348 Format: "duration", 349 }}, 350 // should be exactly equal to maxDurationSizeJSON 351 ExpectedMaxElements: apiservercel.MaxDurationSizeJSON, 352 }, 353 { 354 Name: "dateSize", 355 InputSchema: &spec.Schema{ 356 SchemaProps: spec.SchemaProps{ 357 Type: []string{"string"}, 358 Format: "date", 359 }}, 360 // should be exactly equal to dateSizeJSON 361 ExpectedMaxElements: apiservercel.JSONDateSize, 362 }, 363 { 364 Name: "maxdatetimeSize", 365 InputSchema: &spec.Schema{ 366 SchemaProps: spec.SchemaProps{ 367 Type: []string{"string"}, 368 Format: "date-time", 369 }}, 370 // should be exactly equal to maxDatetimeSizeJSON 371 ExpectedMaxElements: apiservercel.MaxDatetimeSizeJSON, 372 }, 373 { 374 Name: "maxintOrStringSize", 375 InputSchema: &spec.Schema{ 376 VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{ 377 extIntOrString: true, 378 }}}, 379 // should be exactly equal to maxRequestSizeBytes - 2 (to allow for quotes in the case of a string) 380 ExpectedMaxElements: apiservercel.DefaultMaxRequestSizeBytes - 2, 381 }, 382 { 383 Name: "objectDefaultFieldArray", 384 InputSchema: &spec.Schema{ 385 SchemaProps: spec.SchemaProps{ 386 Type: []string{"array"}, 387 Items: &spec.SchemaOrArray{ 388 Schema: &spec.Schema{ 389 SchemaProps: spec.SchemaProps{ 390 Type: []string{"object"}, 391 Properties: map[string]spec.Schema{ 392 "field": {SchemaProps: spec.SchemaProps{ 393 Type: []string{"string"}, 394 Default: "default", 395 }, 396 }}, 397 Required: []string{"field"}, 398 }}}, 399 }, 400 }, 401 // expected JSON is [{},{},...] so our length should be (maxRequestSizeBytes - 2) / 3 402 ExpectedMaxElements: 1048575, 403 }, 404 { 405 Name: "byteStringSize", 406 InputSchema: &spec.Schema{ 407 SchemaProps: spec.SchemaProps{ 408 Type: []string{"string"}, 409 Format: "byte", 410 }}, 411 // expected JSON is "" so our length should be (maxRequestSizeBytes - 2) 412 ExpectedMaxElements: 3145726, 413 }, 414 { 415 Name: "byteStringSetMaxLength", 416 InputSchema: &spec.Schema{ 417 SchemaProps: spec.SchemaProps{ 418 Type: []string{"string"}, 419 Format: "byte", 420 MaxLength: maxPtr(20), 421 }}, 422 // note that unlike regular strings we don't have to take unicode into account, 423 // so we expect the max length to be exactly equal to the user-supplied one 424 ExpectedMaxElements: 20, 425 }, 426 } 427 for _, testCase := range tests { 428 t.Run(testCase.Name, func(t *testing.T) { 429 decl := SchemaDeclType(testCase.InputSchema, false) 430 if decl.MaxElements != testCase.ExpectedMaxElements { 431 t.Errorf("wrong maxElements (got %d, expected %d)", decl.MaxElements, testCase.ExpectedMaxElements) 432 } 433 }) 434 } 435 } 436 437 func genNestedSchema(depth int) *spec.Schema { 438 var generator func(d int) spec.Schema 439 generator = func(d int) spec.Schema { 440 nodeTemplate := &spec.Schema{ 441 SchemaProps: spec.SchemaProps{ 442 Type: []string{"object"}, 443 AdditionalProperties: &spec.SchemaOrBool{}, 444 }} 445 if d == 1 { 446 return *nodeTemplate 447 } else { 448 mapType := generator(d - 1) 449 nodeTemplate.AdditionalProperties.Schema = &mapType 450 return *nodeTemplate 451 } 452 } 453 schema := generator(depth) 454 return &schema 455 } 456 457 func BenchmarkDeeplyNestedSchemaDeclType(b *testing.B) { 458 benchmarkSchema := genNestedSchema(10) 459 b.ResetTimer() 460 for i := 0; i < b.N; i++ { 461 SchemaDeclType(benchmarkSchema, false) 462 } 463 }