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  }