...

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

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model

     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 model
    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  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    28  	apiservercel "k8s.io/apiserver/pkg/cel"
    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.Object != nil {
    47  			pdv := types.DefaultTypeAdapter.NativeToValue(prop.Default.Object)
    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 (prop.ValueValidation == nil || len(prop.ValueValidation.Enum) == 0) && len(f.EnumValues()) != 0 {
    53  			t.Errorf("field had more enum values than the property. field: %s", f.Name)
    54  		}
    55  		if prop.ValueValidation != nil {
    56  			fevs := f.EnumValues()
    57  			for _, fev := range fevs {
    58  				found := false
    59  				for _, pev := range prop.ValueValidation.Enum {
    60  					celpev := types.DefaultTypeAdapter.NativeToValue(pev.Object)
    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  	if ts.ValueValidation != nil {
    75  		for _, name := range ts.ValueValidation.Required {
    76  			df, found := cust.FindField(name)
    77  			if !found {
    78  				t.Errorf("custom type missing required field. field=%s", name)
    79  			}
    80  			if !df.Required {
    81  				t.Errorf("field marked as required in schema, but optional in type. field=%s", df.Name)
    82  			}
    83  		}
    84  	}
    85  }
    86  
    87  func TestSchemaDeclTypes(t *testing.T) {
    88  	ts := testSchema()
    89  	cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject")
    90  	typeMap := apiservercel.FieldTypeMap("CustomObject", cust)
    91  	nested, _ := cust.FindField("nested")
    92  	metadata, _ := cust.FindField("metadata")
    93  	expectedObjTypeMap := map[string]*apiservercel.DeclType{
    94  		"CustomObject":          cust,
    95  		"CustomObject.nested":   nested.Type,
    96  		"CustomObject.metadata": metadata.Type,
    97  	}
    98  	objTypeMap := map[string]*apiservercel.DeclType{}
    99  	for name, t := range typeMap {
   100  		if t.IsObject() {
   101  			objTypeMap[name] = t
   102  		}
   103  	}
   104  	if len(objTypeMap) != len(expectedObjTypeMap) {
   105  		t.Errorf("got different type set. got=%v, wanted=%v", objTypeMap, expectedObjTypeMap)
   106  	}
   107  	for exp, expType := range expectedObjTypeMap {
   108  		actType, found := objTypeMap[exp]
   109  		if !found {
   110  			t.Errorf("missing type in rule types: %s", exp)
   111  			continue
   112  		}
   113  		expT, err := expType.ExprType()
   114  		if err != nil {
   115  			t.Errorf("fail to get cel type: %s", err)
   116  		}
   117  		actT, err := actType.ExprType()
   118  		if err != nil {
   119  			t.Errorf("fail to get cel type: %s", err)
   120  		}
   121  		if !proto.Equal(expT, actT) {
   122  			t.Errorf("incompatible CEL types. got=%v, wanted=%v", expT, actT)
   123  		}
   124  	}
   125  }
   126  
   127  func testSchema() *schema.Structural {
   128  	// Manual construction of a schema with the following definition:
   129  	//
   130  	// schema:
   131  	//   type: object
   132  	//   metadata:
   133  	//     custom_type: "CustomObject"
   134  	//   required:
   135  	//     - name
   136  	//     - value
   137  	//   properties:
   138  	//     name:
   139  	//       type: string
   140  	//     nested:
   141  	//       type: object
   142  	//       properties:
   143  	//         subname:
   144  	//           type: string
   145  	//         flags:
   146  	//           type: object
   147  	//           additionalProperties:
   148  	//             type: boolean
   149  	//         dates:
   150  	//           type: array
   151  	//           items:
   152  	//             type: string
   153  	//             format: date-time
   154  	//      metadata:
   155  	//        type: object
   156  	//        additionalProperties:
   157  	//          type: object
   158  	//          properties:
   159  	//            key:
   160  	//              type: string
   161  	//            values:
   162  	//              type: array
   163  	//              items: string
   164  	//     value:
   165  	//       type: integer
   166  	//       format: int64
   167  	//       default: 1
   168  	//       enum: [1,2,3]
   169  	ts := &schema.Structural{
   170  		Generic: schema.Generic{
   171  			Type: "object",
   172  		},
   173  		Properties: map[string]schema.Structural{
   174  			"name": {
   175  				Generic: schema.Generic{
   176  					Type: "string",
   177  				},
   178  			},
   179  			"value": {
   180  				Generic: schema.Generic{
   181  					Type:    "integer",
   182  					Default: schema.JSON{Object: int64(1)},
   183  				},
   184  				ValueValidation: &schema.ValueValidation{
   185  					Format: "int64",
   186  					Enum:   []schema.JSON{{Object: int64(1)}, {Object: int64(2)}, {Object: int64(3)}},
   187  				},
   188  			},
   189  			"nested": {
   190  				Generic: schema.Generic{
   191  					Type: "object",
   192  				},
   193  				Properties: map[string]schema.Structural{
   194  					"subname": {
   195  						Generic: schema.Generic{
   196  							Type: "string",
   197  						},
   198  					},
   199  					"flags": {
   200  						Generic: schema.Generic{
   201  							Type: "object",
   202  							AdditionalProperties: &schema.StructuralOrBool{
   203  								Structural: &schema.Structural{
   204  									Generic: schema.Generic{
   205  										Type: "boolean",
   206  									},
   207  								},
   208  							},
   209  						},
   210  					},
   211  					"dates": {
   212  						Generic: schema.Generic{
   213  							Type: "array",
   214  						},
   215  						Items: &schema.Structural{
   216  							Generic: schema.Generic{
   217  								Type: "string",
   218  							},
   219  							ValueValidation: &schema.ValueValidation{
   220  								Format: "date-time",
   221  							},
   222  						},
   223  					},
   224  				},
   225  			},
   226  			"metadata": {
   227  				Generic: schema.Generic{
   228  					Type: "object",
   229  				},
   230  				Properties: map[string]schema.Structural{
   231  					"name": {
   232  						Generic: schema.Generic{
   233  							Type: "string",
   234  						},
   235  					},
   236  					"value": {
   237  						Generic: schema.Generic{
   238  							Type: "array",
   239  						},
   240  						Items: &schema.Structural{
   241  							Generic: schema.Generic{
   242  								Type: "string",
   243  							},
   244  						},
   245  					},
   246  				},
   247  			},
   248  		},
   249  	}
   250  	return ts
   251  }
   252  
   253  func arraySchema(arrayType, format string, maxItems *int64) *schema.Structural {
   254  	return &schema.Structural{
   255  		Generic: schema.Generic{
   256  			Type: "array",
   257  		},
   258  		Items: &schema.Structural{
   259  			Generic: schema.Generic{
   260  				Type: arrayType,
   261  			},
   262  			ValueValidation: &schema.ValueValidation{
   263  				Format: format,
   264  			},
   265  		},
   266  		ValueValidation: &schema.ValueValidation{
   267  			MaxItems: maxItems,
   268  		},
   269  	}
   270  }
   271  
   272  func TestEstimateMaxLengthJSON(t *testing.T) {
   273  	type maxLengthTest struct {
   274  		Name                string
   275  		InputSchema         *schema.Structural
   276  		ExpectedMaxElements int64
   277  	}
   278  	tests := []maxLengthTest{
   279  		{
   280  			Name:        "booleanArray",
   281  			InputSchema: arraySchema("boolean", "", nil),
   282  			// expected JSON is [true,true,...], so our length should be (maxRequestSizeBytes - 2) / 5
   283  			ExpectedMaxElements: 629145,
   284  		},
   285  		{
   286  			Name:        "durationArray",
   287  			InputSchema: arraySchema("string", "duration", nil),
   288  			// expected JSON is ["0","0",...] so our length should be (maxRequestSizeBytes - 2) / 4
   289  			ExpectedMaxElements: 786431,
   290  		},
   291  		{
   292  			Name:        "datetimeArray",
   293  			InputSchema: arraySchema("string", "date-time", nil),
   294  			// expected JSON is ["2000-01-01T01:01:01","2000-01-01T01:01:01",...] so our length should be (maxRequestSizeBytes - 2) / 22
   295  			ExpectedMaxElements: 142987,
   296  		},
   297  		{
   298  			Name:        "dateArray",
   299  			InputSchema: arraySchema("string", "date", nil),
   300  			// expected JSON is ["2000-01-01","2000-01-02",...] so our length should be (maxRequestSizeBytes - 2) / 13
   301  			ExpectedMaxElements: 241978,
   302  		},
   303  		{
   304  			Name:        "numberArray",
   305  			InputSchema: arraySchema("integer", "", nil),
   306  			// expected JSON is [0,0,...] so our length should be (maxRequestSizeBytes - 2) / 2
   307  			ExpectedMaxElements: 1572863,
   308  		},
   309  		{
   310  			Name:        "stringArray",
   311  			InputSchema: arraySchema("string", "", nil),
   312  			// expected JSON is ["","",...] so our length should be (maxRequestSizeBytes - 2) / 3
   313  			ExpectedMaxElements: 1048575,
   314  		},
   315  		{
   316  			Name: "stringMap",
   317  			InputSchema: &schema.Structural{
   318  				Generic: schema.Generic{
   319  					Type: "object",
   320  					AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{
   321  						Generic: schema.Generic{
   322  							Type: "string",
   323  						},
   324  					}},
   325  				},
   326  			},
   327  			// expected JSON is {"":"","":"",...} so our length should be (3000000 - 2) / 6
   328  			ExpectedMaxElements: 393215,
   329  		},
   330  		{
   331  			Name: "objectOptionalPropertyArray",
   332  			InputSchema: &schema.Structural{
   333  				Generic: schema.Generic{
   334  					Type: "array",
   335  				},
   336  				Items: &schema.Structural{
   337  					Generic: schema.Generic{
   338  						Type: "object",
   339  					},
   340  					Properties: map[string]schema.Structural{
   341  						"required": {
   342  							Generic: schema.Generic{
   343  								Type: "string",
   344  							},
   345  						},
   346  						"optional": {
   347  							Generic: schema.Generic{
   348  								Type: "string",
   349  							},
   350  						},
   351  					},
   352  					ValueValidation: &schema.ValueValidation{
   353  						Required: []string{"required"},
   354  					},
   355  				},
   356  			},
   357  			// expected JSON is [{"required":"",},{"required":"",},...] so our length should be (maxRequestSizeBytes - 2) / 17
   358  			ExpectedMaxElements: 185042,
   359  		},
   360  		{
   361  			Name:        "arrayWithLength",
   362  			InputSchema: arraySchema("integer", "int64", maxPtr(10)),
   363  			// manually set by MaxItems
   364  			ExpectedMaxElements: 10,
   365  		},
   366  		{
   367  			Name: "stringWithLength",
   368  			InputSchema: &schema.Structural{
   369  				Generic: schema.Generic{
   370  					Type: "string",
   371  				},
   372  				ValueValidation: &schema.ValueValidation{
   373  					MaxLength: maxPtr(20),
   374  				},
   375  			},
   376  			// manually set by MaxLength, but we expect a 4x multiplier compared to the original input
   377  			// since OpenAPIv3 maxLength uses code points, but DeclType works with bytes
   378  			ExpectedMaxElements: 80,
   379  		},
   380  		{
   381  			Name: "mapWithLength",
   382  			InputSchema: &schema.Structural{
   383  				Generic: schema.Generic{
   384  					Type: "object",
   385  					AdditionalProperties: &schema.StructuralOrBool{Structural: &schema.Structural{
   386  						Generic: schema.Generic{
   387  							Type: "string",
   388  						},
   389  					}},
   390  				},
   391  				ValueValidation: &schema.ValueValidation{
   392  					Format:        "string",
   393  					MaxProperties: maxPtr(15),
   394  				},
   395  			},
   396  			// manually set by MaxProperties
   397  			ExpectedMaxElements: 15,
   398  		},
   399  		{
   400  			Name: "durationMaxSize",
   401  			InputSchema: &schema.Structural{
   402  				Generic: schema.Generic{
   403  					Type: "string",
   404  				},
   405  				ValueValidation: &schema.ValueValidation{
   406  					Format: "duration",
   407  				},
   408  			},
   409  			// should be exactly equal to maxDurationSizeJSON
   410  			ExpectedMaxElements: apiservercel.MaxDurationSizeJSON,
   411  		},
   412  		{
   413  			Name: "dateSize",
   414  			InputSchema: &schema.Structural{
   415  				Generic: schema.Generic{
   416  					Type: "string",
   417  				},
   418  				ValueValidation: &schema.ValueValidation{
   419  					Format: "date",
   420  				},
   421  			},
   422  			// should be exactly equal to dateSizeJSON
   423  			ExpectedMaxElements: apiservercel.JSONDateSize,
   424  		},
   425  		{
   426  			Name: "maxdatetimeSize",
   427  			InputSchema: &schema.Structural{
   428  				Generic: schema.Generic{
   429  					Type: "string",
   430  				},
   431  				ValueValidation: &schema.ValueValidation{
   432  					Format: "date-time",
   433  				},
   434  			},
   435  			// should be exactly equal to maxDatetimeSizeJSON
   436  			ExpectedMaxElements: apiservercel.MaxDatetimeSizeJSON,
   437  		},
   438  		{
   439  			Name: "maxintOrStringSize",
   440  			InputSchema: &schema.Structural{
   441  				Extensions: schema.Extensions{
   442  					XIntOrString: true,
   443  				},
   444  			},
   445  			// should be exactly equal to maxRequestSizeBytes - 2 (to allow for quotes in the case of a string)
   446  			ExpectedMaxElements: apiservercel.DefaultMaxRequestSizeBytes - 2,
   447  		},
   448  		{
   449  			Name: "objectDefaultFieldArray",
   450  			InputSchema: &schema.Structural{
   451  				Generic: schema.Generic{
   452  					Type: "array",
   453  				},
   454  				Items: &schema.Structural{
   455  					Generic: schema.Generic{
   456  						Type: "object",
   457  					},
   458  					Properties: map[string]schema.Structural{
   459  						"field": {
   460  							Generic: schema.Generic{
   461  								Type:    "string",
   462  								Default: schema.JSON{Object: "default"},
   463  							},
   464  						},
   465  					},
   466  					ValueValidation: &schema.ValueValidation{
   467  						Required: []string{"field"},
   468  					},
   469  				},
   470  			},
   471  			// expected JSON is [{},{},...] so our length should be (maxRequestSizeBytes - 2) / 3
   472  			ExpectedMaxElements: 1048575,
   473  		},
   474  		{
   475  			Name: "byteStringSize",
   476  			InputSchema: &schema.Structural{
   477  				Generic: schema.Generic{
   478  					Type: "string",
   479  				},
   480  				ValueValidation: &schema.ValueValidation{
   481  					Format: "byte",
   482  				},
   483  			},
   484  			// expected JSON is "" so our length should be (maxRequestSizeBytes - 2)
   485  			ExpectedMaxElements: 3145726,
   486  		},
   487  		{
   488  			Name: "byteStringSetMaxLength",
   489  			InputSchema: &schema.Structural{
   490  				Generic: schema.Generic{
   491  					Type: "string",
   492  				},
   493  				ValueValidation: &schema.ValueValidation{
   494  					Format:    "byte",
   495  					MaxLength: maxPtr(20),
   496  				},
   497  			},
   498  			// note that unlike regular strings we don't have to take unicode into account,
   499  			// so we expect the max length to be exactly equal to the user-supplied one
   500  			ExpectedMaxElements: 20,
   501  		},
   502  	}
   503  	for _, testCase := range tests {
   504  		t.Run(testCase.Name, func(t *testing.T) {
   505  			decl := SchemaDeclType(testCase.InputSchema, false)
   506  			if decl.MaxElements != testCase.ExpectedMaxElements {
   507  				t.Errorf("wrong maxElements (got %d, expected %d)", decl.MaxElements, testCase.ExpectedMaxElements)
   508  			}
   509  		})
   510  	}
   511  }
   512  
   513  func maxPtr(max int64) *int64 {
   514  	return &max
   515  }
   516  
   517  func genNestedSchema(depth int) *schema.Structural {
   518  	var generator func(d int) schema.Structural
   519  	generator = func(d int) schema.Structural {
   520  		nodeTemplate := schema.Structural{
   521  			Generic: schema.Generic{
   522  				Type:                 "object",
   523  				AdditionalProperties: &schema.StructuralOrBool{},
   524  			},
   525  		}
   526  		if d == 1 {
   527  			return nodeTemplate
   528  		} else {
   529  			mapType := generator(d - 1)
   530  			nodeTemplate.Generic.AdditionalProperties.Structural = &mapType
   531  			return nodeTemplate
   532  		}
   533  	}
   534  	schema := generator(depth)
   535  	return &schema
   536  }
   537  
   538  func BenchmarkDeeplyNestedSchemaDeclType(b *testing.B) {
   539  	benchmarkSchema := genNestedSchema(10)
   540  	b.ResetTimer()
   541  	for i := 0; i < b.N; i++ {
   542  		SchemaDeclType(benchmarkSchema, false)
   543  	}
   544  }
   545  

View as plain text