...

Source file src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation_test.go

Documentation: k8s.io/apiextensions-apiserver/pkg/apiserver/validation

     1  /*
     2  Copyright 2017 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 validation
    18  
    19  import (
    20  	"context"
    21  	"math/rand"
    22  	"os"
    23  	"strconv"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  
    29  	utilpointer "k8s.io/utils/pointer"
    30  	kjson "sigs.k8s.io/json"
    31  
    32  	kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
    33  
    34  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    35  	apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
    36  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    37  	structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
    38  	"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
    39  	"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
    40  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    41  	"k8s.io/apimachinery/pkg/runtime"
    42  	"k8s.io/apimachinery/pkg/runtime/serializer"
    43  	"k8s.io/apimachinery/pkg/util/json"
    44  	"k8s.io/apimachinery/pkg/util/sets"
    45  	celconfig "k8s.io/apiserver/pkg/apis/cel"
    46  )
    47  
    48  // TestRoundTrip checks the conversion to go-openapi types.
    49  // internal -> go-openapi -> JSON -> external -> internal
    50  func TestRoundTrip(t *testing.T) {
    51  	scheme := runtime.NewScheme()
    52  	codecs := serializer.NewCodecFactory(scheme)
    53  
    54  	// add internal and external types to scheme
    55  	if err := apiextensions.AddToScheme(scheme); err != nil {
    56  		t.Fatal(err)
    57  	}
    58  	if err := apiextensionsv1.AddToScheme(scheme); err != nil {
    59  		t.Fatal(err)
    60  	}
    61  
    62  	seed := int64(time.Now().Nanosecond())
    63  	if override := os.Getenv("TEST_RAND_SEED"); len(override) > 0 {
    64  		overrideSeed, err := strconv.Atoi(override)
    65  		if err != nil {
    66  			t.Fatal(err)
    67  		}
    68  		seed = int64(overrideSeed)
    69  		t.Logf("using overridden seed: %d", seed)
    70  	} else {
    71  		t.Logf("seed (override with TEST_RAND_SEED if desired): %d", seed)
    72  	}
    73  	fuzzerFuncs := fuzzer.MergeFuzzerFuncs(apiextensionsfuzzer.Funcs)
    74  	f := fuzzer.FuzzerFor(fuzzerFuncs, rand.NewSource(seed), codecs)
    75  
    76  	for i := 0; i < 50; i++ {
    77  		// fuzz internal types
    78  		internal := &apiextensions.JSONSchemaProps{}
    79  		f.Fuzz(internal)
    80  
    81  		// internal -> go-openapi
    82  		openAPITypes := &kubeopenapispec.Schema{}
    83  		if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
    84  			t.Fatal(err)
    85  		}
    86  
    87  		// go-openapi -> JSON
    88  		openAPIJSON, err := json.Marshal(openAPITypes)
    89  		if err != nil {
    90  			t.Fatal(err)
    91  		}
    92  
    93  		// JSON -> in-memory JSON => convertNullTypeToNullable => JSON
    94  		var j interface{}
    95  		if strictErrs, err := kjson.UnmarshalStrict(openAPIJSON, &j); err != nil {
    96  			t.Fatal(err)
    97  		} else if len(strictErrs) > 0 {
    98  			t.Fatal(strictErrs)
    99  		}
   100  		j = stripIntOrStringType(j)
   101  		openAPIJSON, err = json.Marshal(j)
   102  		if err != nil {
   103  			t.Fatal(err)
   104  		}
   105  
   106  		// JSON -> external
   107  		external := &apiextensionsv1.JSONSchemaProps{}
   108  		if strictErrs, err := kjson.UnmarshalStrict(openAPIJSON, external); err != nil {
   109  			t.Fatal(err)
   110  		} else if len(strictErrs) > 0 {
   111  			t.Fatal(strictErrs)
   112  		}
   113  
   114  		// external -> internal
   115  		internalRoundTripped := &apiextensions.JSONSchemaProps{}
   116  		if err := scheme.Convert(external, internalRoundTripped, nil); err != nil {
   117  			t.Fatal(err)
   118  		}
   119  
   120  		if !apiequality.Semantic.DeepEqual(internal, internalRoundTripped) {
   121  			t.Log(string(openAPIJSON))
   122  			t.Fatalf("%d: unexpected diff\n\t%s", i, cmp.Diff(internal, internalRoundTripped))
   123  		}
   124  	}
   125  }
   126  
   127  func stripIntOrStringType(x interface{}) interface{} {
   128  	switch x := x.(type) {
   129  	case map[string]interface{}:
   130  		if t, found := x["type"]; found {
   131  			switch t := t.(type) {
   132  			case []interface{}:
   133  				if len(t) == 2 && t[0] == "integer" && t[1] == "string" && x["x-kubernetes-int-or-string"] == true {
   134  					delete(x, "type")
   135  				}
   136  			}
   137  		}
   138  		for k := range x {
   139  			x[k] = stripIntOrStringType(x[k])
   140  		}
   141  		return x
   142  	case []interface{}:
   143  		for i := range x {
   144  			x[i] = stripIntOrStringType(x[i])
   145  		}
   146  		return x
   147  	default:
   148  		return x
   149  	}
   150  }
   151  
   152  type failingObject struct {
   153  	object     interface{}
   154  	oldObject  interface{}
   155  	expectErrs []string
   156  }
   157  
   158  func TestValidateCustomResource(t *testing.T) {
   159  	tests := []struct {
   160  		name           string
   161  		schema         apiextensions.JSONSchemaProps
   162  		objects        []interface{}
   163  		oldObjects     []interface{}
   164  		failingObjects []failingObject
   165  	}{
   166  		{name: "!nullable",
   167  			schema: apiextensions.JSONSchemaProps{
   168  				Type: "object",
   169  				Properties: map[string]apiextensions.JSONSchemaProps{
   170  					"field": {
   171  						Type:     "object",
   172  						Nullable: false,
   173  					},
   174  				},
   175  			},
   176  			objects: []interface{}{
   177  				map[string]interface{}{},
   178  				map[string]interface{}{"field": map[string]interface{}{}},
   179  			},
   180  			failingObjects: []failingObject{
   181  				{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
   182  				{object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
   183  				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
   184  				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
   185  				{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type object: "array"`}},
   186  				{object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type object: "null"`}},
   187  			},
   188  		},
   189  		{name: "nullable",
   190  			schema: apiextensions.JSONSchemaProps{
   191  				Type: "object",
   192  				Properties: map[string]apiextensions.JSONSchemaProps{
   193  					"field": {
   194  						Type:     "object",
   195  						Nullable: true,
   196  					},
   197  				},
   198  			},
   199  			objects: []interface{}{
   200  				map[string]interface{}{},
   201  				map[string]interface{}{"field": map[string]interface{}{}},
   202  				map[string]interface{}{"field": nil},
   203  			},
   204  			failingObjects: []failingObject{
   205  				{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{`field: Invalid value: "string": field in body must be of type object: "string"`}},
   206  				{object: map[string]interface{}{"field": 42}, expectErrs: []string{`field: Invalid value: "integer": field in body must be of type object: "integer"`}},
   207  				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type object: "boolean"`}},
   208  				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type object: "number"`}},
   209  				{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type object: "array"`}},
   210  			},
   211  		},
   212  		{name: "nullable and no type",
   213  			schema: apiextensions.JSONSchemaProps{
   214  				Type: "object",
   215  				Properties: map[string]apiextensions.JSONSchemaProps{
   216  					"field": {
   217  						Nullable: true,
   218  					},
   219  				},
   220  			},
   221  			objects: []interface{}{
   222  				map[string]interface{}{},
   223  				map[string]interface{}{"field": map[string]interface{}{}},
   224  				map[string]interface{}{"field": nil},
   225  				map[string]interface{}{"field": "foo"},
   226  				map[string]interface{}{"field": 42},
   227  				map[string]interface{}{"field": true},
   228  				map[string]interface{}{"field": 1.2},
   229  				map[string]interface{}{"field": []interface{}{}},
   230  			},
   231  		},
   232  		{name: "x-kubernetes-int-or-string",
   233  			schema: apiextensions.JSONSchemaProps{
   234  				Type: "object",
   235  				Properties: map[string]apiextensions.JSONSchemaProps{
   236  					"field": {
   237  						XIntOrString: true,
   238  					},
   239  				},
   240  			},
   241  			objects: []interface{}{
   242  				map[string]interface{}{},
   243  				map[string]interface{}{"field": 42},
   244  				map[string]interface{}{"field": "foo"},
   245  			},
   246  			failingObjects: []failingObject{
   247  				{object: map[string]interface{}{"field": nil}, expectErrs: []string{`field: Invalid value: "null": field in body must be of type integer,string: "null"`}},
   248  				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
   249  				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
   250  				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
   251  				{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type integer,string: "array"`}},
   252  			},
   253  		},
   254  		{name: "nullable and x-kubernetes-int-or-string",
   255  			schema: apiextensions.JSONSchemaProps{
   256  				Type: "object",
   257  				Properties: map[string]apiextensions.JSONSchemaProps{
   258  					"field": {
   259  						Nullable:     true,
   260  						XIntOrString: true,
   261  					},
   262  				},
   263  			},
   264  			objects: []interface{}{
   265  				map[string]interface{}{},
   266  				map[string]interface{}{"field": 42},
   267  				map[string]interface{}{"field": "foo"},
   268  				map[string]interface{}{"field": nil},
   269  			},
   270  			failingObjects: []failingObject{
   271  				{object: map[string]interface{}{"field": true}, expectErrs: []string{`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`}},
   272  				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{`field: Invalid value: "number": field in body must be of type integer,string: "number"`}},
   273  				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field: Invalid value: "object": field in body must be of type integer,string: "object"`}},
   274  				{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{`field: Invalid value: "array": field in body must be of type integer,string: "array"`}},
   275  			},
   276  		},
   277  		{name: "nullable, x-kubernetes-int-or-string and user-provided anyOf",
   278  			schema: apiextensions.JSONSchemaProps{
   279  				Type: "object",
   280  				Properties: map[string]apiextensions.JSONSchemaProps{
   281  					"field": {
   282  						Nullable:     true,
   283  						XIntOrString: true,
   284  						AnyOf: []apiextensions.JSONSchemaProps{
   285  							{Type: "integer"},
   286  							{Type: "string"},
   287  						},
   288  					},
   289  				},
   290  			},
   291  			objects: []interface{}{
   292  				map[string]interface{}{},
   293  				map[string]interface{}{"field": nil},
   294  				map[string]interface{}{"field": 42},
   295  				map[string]interface{}{"field": "foo"},
   296  			},
   297  			failingObjects: []failingObject{
   298  				{object: map[string]interface{}{"field": true}, expectErrs: []string{
   299  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   300  					`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`,
   301  					`field: Invalid value: "boolean": field in body must be of type integer: "boolean"`,
   302  				}},
   303  				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{
   304  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   305  					`field: Invalid value: "number": field in body must be of type integer,string: "number"`,
   306  					`field: Invalid value: "number": field in body must be of type integer: "number"`,
   307  				}},
   308  				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
   309  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   310  					`field: Invalid value: "object": field in body must be of type integer,string: "object"`,
   311  					`field: Invalid value: "object": field in body must be of type integer: "object"`,
   312  				}},
   313  				{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{
   314  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   315  					`field: Invalid value: "array": field in body must be of type integer,string: "array"`,
   316  					`field: Invalid value: "array": field in body must be of type integer: "array"`,
   317  				}},
   318  			},
   319  		},
   320  		{name: "nullable, x-kubernetes-int-or-string and user-provider allOf",
   321  			schema: apiextensions.JSONSchemaProps{
   322  				Type: "object",
   323  				Properties: map[string]apiextensions.JSONSchemaProps{
   324  					"field": {
   325  						Nullable:     true,
   326  						XIntOrString: true,
   327  						AllOf: []apiextensions.JSONSchemaProps{
   328  							{
   329  								AnyOf: []apiextensions.JSONSchemaProps{
   330  									{Type: "integer"},
   331  									{Type: "string"},
   332  								},
   333  							},
   334  						},
   335  					},
   336  				},
   337  			},
   338  			objects: []interface{}{
   339  				map[string]interface{}{},
   340  				map[string]interface{}{"field": nil},
   341  				map[string]interface{}{"field": 42},
   342  				map[string]interface{}{"field": "foo"},
   343  			},
   344  			failingObjects: []failingObject{
   345  				{object: map[string]interface{}{"field": true}, expectErrs: []string{
   346  					`<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
   347  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   348  					`field: Invalid value: "boolean": field in body must be of type integer,string: "boolean"`,
   349  					`field: Invalid value: "boolean": field in body must be of type integer: "boolean"`,
   350  				}},
   351  				{object: map[string]interface{}{"field": 1.2}, expectErrs: []string{
   352  					`<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
   353  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   354  					`field: Invalid value: "number": field in body must be of type integer,string: "number"`,
   355  					`field: Invalid value: "number": field in body must be of type integer: "number"`,
   356  				}},
   357  				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
   358  					`<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
   359  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   360  					`field: Invalid value: "object": field in body must be of type integer,string: "object"`,
   361  					`field: Invalid value: "object": field in body must be of type integer: "object"`,
   362  				}},
   363  				{object: map[string]interface{}{"field": []interface{}{}}, expectErrs: []string{
   364  					`<nil>: Invalid value: "": "field" must validate all the schemas (allOf). None validated`,
   365  					`<nil>: Invalid value: "": "field" must validate at least one schema (anyOf)`,
   366  					`field: Invalid value: "array": field in body must be of type integer,string: "array"`,
   367  					`field: Invalid value: "array": field in body must be of type integer: "array"`,
   368  				}},
   369  			},
   370  		},
   371  		{name: "invalid regex",
   372  			schema: apiextensions.JSONSchemaProps{
   373  				Type: "object",
   374  				Properties: map[string]apiextensions.JSONSchemaProps{
   375  					"field": {
   376  						Type:    "string",
   377  						Pattern: "+",
   378  					},
   379  				},
   380  			},
   381  			failingObjects: []failingObject{
   382  				{object: map[string]interface{}{"field": "foo"}, expectErrs: []string{"field: Invalid value: \"foo\": field in body should match '+, but pattern is invalid: error parsing regexp: missing argument to repetition operator: `+`'"}},
   383  			},
   384  		},
   385  		{name: "required field",
   386  			schema: apiextensions.JSONSchemaProps{
   387  				Type:     "object",
   388  				Required: []string{"field"},
   389  				Properties: map[string]apiextensions.JSONSchemaProps{
   390  					"field": {
   391  						Type:     "object",
   392  						Required: []string{"nested"},
   393  						Properties: map[string]apiextensions.JSONSchemaProps{
   394  							"nested": {},
   395  						},
   396  					},
   397  				},
   398  			},
   399  			failingObjects: []failingObject{
   400  				{object: map[string]interface{}{"test": "a"}, expectErrs: []string{`field: Required value`}},
   401  				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{`field.nested: Required value`}},
   402  			},
   403  		},
   404  		{name: "enum",
   405  			schema: apiextensions.JSONSchemaProps{
   406  				Type: "object",
   407  				Properties: map[string]apiextensions.JSONSchemaProps{
   408  					"field": {
   409  						Type:     "object",
   410  						Required: []string{"nestedint", "nestedstring"},
   411  						Properties: map[string]apiextensions.JSONSchemaProps{
   412  							"nestedint": {
   413  								Type: "integer",
   414  								Enum: []apiextensions.JSON{1, 2},
   415  							},
   416  							"nestedstring": {
   417  								Type: "string",
   418  								Enum: []apiextensions.JSON{"a", "b"},
   419  							},
   420  						},
   421  					},
   422  				},
   423  			},
   424  			failingObjects: []failingObject{
   425  				{object: map[string]interface{}{"field": map[string]interface{}{}}, expectErrs: []string{
   426  					`field.nestedint: Required value`,
   427  					`field.nestedstring: Required value`,
   428  				}},
   429  				{object: map[string]interface{}{"field": map[string]interface{}{"nestedint": "x", "nestedstring": true}}, expectErrs: []string{
   430  					`field.nestedint: Invalid value: "string": field.nestedint in body must be of type integer: "string"`,
   431  					`field.nestedint: Unsupported value: "x": supported values: "1", "2"`,
   432  					`field.nestedstring: Invalid value: "boolean": field.nestedstring in body must be of type string: "boolean"`,
   433  					`field.nestedstring: Unsupported value: true: supported values: "a", "b"`,
   434  				}},
   435  			},
   436  		},
   437  		{name: "immutability transition rule",
   438  			schema: apiextensions.JSONSchemaProps{
   439  				Type: "object",
   440  				Properties: map[string]apiextensions.JSONSchemaProps{
   441  					"field": {
   442  						Type: "string",
   443  						XValidations: []apiextensions.ValidationRule{
   444  							{
   445  								Rule: "self == oldSelf",
   446  							},
   447  						},
   448  					},
   449  				},
   450  			},
   451  			objects: []interface{}{
   452  				map[string]interface{}{"field": "x"},
   453  			},
   454  			oldObjects: []interface{}{
   455  				map[string]interface{}{"field": "x"},
   456  			},
   457  			failingObjects: []failingObject{
   458  				{
   459  					object:    map[string]interface{}{"field": "y"},
   460  					oldObject: map[string]interface{}{"field": "x"},
   461  					expectErrs: []string{
   462  						`field: Invalid value: "string": failed rule: self == oldSelf`,
   463  					}},
   464  			},
   465  		},
   466  		{name: "correlatable transition rule",
   467  			// Ensures a transition rule under a "listMap" is supported.
   468  			schema: apiextensions.JSONSchemaProps{
   469  				Type: "object",
   470  				Properties: map[string]apiextensions.JSONSchemaProps{
   471  					"field": {
   472  						Type:         "array",
   473  						XListType:    &listMapType,
   474  						XListMapKeys: []string{"k1", "k2"},
   475  						Items: &apiextensions.JSONSchemaPropsOrArray{
   476  							Schema: &apiextensions.JSONSchemaProps{
   477  								Type: "object",
   478  								Properties: map[string]apiextensions.JSONSchemaProps{
   479  									"k1": {
   480  										Type: "string",
   481  									},
   482  									"k2": {
   483  										Type: "string",
   484  									},
   485  									"v1": {
   486  										Type: "number",
   487  										XValidations: []apiextensions.ValidationRule{
   488  											{
   489  												Rule: "self >= oldSelf",
   490  											},
   491  										},
   492  									},
   493  								},
   494  							},
   495  						},
   496  					},
   497  				},
   498  			},
   499  			objects: []interface{}{
   500  				map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.2}}},
   501  			},
   502  			oldObjects: []interface{}{
   503  				map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.0}}},
   504  			},
   505  			failingObjects: []failingObject{
   506  				{
   507  					object:    map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 0.9}}},
   508  					oldObject: map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.0}}},
   509  					expectErrs: []string{
   510  						`field[0].v1: Invalid value: "number": failed rule: self >= oldSelf`,
   511  					}},
   512  			},
   513  		},
   514  		{name: "validation rule under non-correlatable field",
   515  			// The array makes the rule on the nested string non-correlatable
   516  			// for transition rule purposes. This test ensures that a rule that
   517  			// does NOT use oldSelf (is not a transition rule), still behaves
   518  			// as expected under a non-correlatable field.
   519  			schema: apiextensions.JSONSchemaProps{
   520  				Type: "object",
   521  				Properties: map[string]apiextensions.JSONSchemaProps{
   522  					"field": {
   523  						Type: "array",
   524  						Items: &apiextensions.JSONSchemaPropsOrArray{
   525  							Schema: &apiextensions.JSONSchemaProps{
   526  								Type: "object",
   527  								Properties: map[string]apiextensions.JSONSchemaProps{
   528  									"x": {
   529  										Type: "string",
   530  										XValidations: []apiextensions.ValidationRule{
   531  											{
   532  												Rule: "self == 'x'",
   533  											},
   534  										},
   535  									},
   536  								},
   537  							},
   538  						},
   539  					},
   540  				},
   541  			},
   542  			objects: []interface{}{
   543  				map[string]interface{}{"field": []interface{}{map[string]interface{}{"x": "x"}}},
   544  			},
   545  			failingObjects: []failingObject{
   546  				{
   547  					object: map[string]interface{}{"field": []interface{}{map[string]interface{}{"x": "y"}}},
   548  					expectErrs: []string{
   549  						`field[0].x: Invalid value: "string": failed rule: self == 'x'`,
   550  					}},
   551  			},
   552  		},
   553  		{name: "maxProperties",
   554  			schema: apiextensions.JSONSchemaProps{
   555  				Type: "object",
   556  				Properties: map[string]apiextensions.JSONSchemaProps{
   557  					"fieldX": {
   558  						Type:          "object",
   559  						MaxProperties: utilpointer.Int64(2),
   560  					},
   561  				},
   562  			},
   563  			failingObjects: []failingObject{
   564  				{object: map[string]interface{}{"fieldX": map[string]interface{}{"a": true, "b": true, "c": true}}, expectErrs: []string{
   565  					`fieldX: Too many: 3: must have at most 2 items`,
   566  				}},
   567  			},
   568  		},
   569  		{name: "maxItems",
   570  			schema: apiextensions.JSONSchemaProps{
   571  				Type: "object",
   572  				Properties: map[string]apiextensions.JSONSchemaProps{
   573  					"fieldX": {
   574  						Type:     "array",
   575  						MaxItems: utilpointer.Int64(2),
   576  					},
   577  				},
   578  			},
   579  			failingObjects: []failingObject{
   580  				{object: map[string]interface{}{"fieldX": []interface{}{"a", "b", "c"}}, expectErrs: []string{
   581  					`fieldX: Too many: 3: must have at most 2 items`,
   582  				}},
   583  			},
   584  		},
   585  		{name: "maxLength",
   586  			schema: apiextensions.JSONSchemaProps{
   587  				Type: "object",
   588  				Properties: map[string]apiextensions.JSONSchemaProps{
   589  					"fieldX": {
   590  						Type:      "string",
   591  						MaxLength: utilpointer.Int64(2),
   592  					},
   593  				},
   594  			},
   595  			failingObjects: []failingObject{
   596  				{object: map[string]interface{}{"fieldX": "abc"}, expectErrs: []string{
   597  					`fieldX: Too long: may not be longer than 2`,
   598  				}},
   599  			},
   600  		},
   601  	}
   602  	for _, tt := range tests {
   603  		t.Run(tt.name, func(t *testing.T) {
   604  			validator, _, err := NewSchemaValidator(&tt.schema)
   605  			if err != nil {
   606  				t.Fatal(err)
   607  			}
   608  			structural, err := structuralschema.NewStructural(&tt.schema)
   609  			if err != nil {
   610  				t.Fatal(err)
   611  			}
   612  			celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit)
   613  			for i, obj := range tt.objects {
   614  				var oldObject interface{}
   615  				if len(tt.oldObjects) == len(tt.objects) {
   616  					oldObject = tt.oldObjects[i]
   617  				}
   618  				if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
   619  					t.Errorf("unexpected validation error for %v: %v", obj, errs)
   620  				}
   621  				errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, celconfig.RuntimeCELCostBudget)
   622  				if len(errs) > 0 {
   623  					t.Errorf(errs.ToAggregate().Error())
   624  				}
   625  			}
   626  			for i, failingObject := range tt.failingObjects {
   627  				errs := ValidateCustomResource(nil, failingObject.object, validator)
   628  				celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, celconfig.RuntimeCELCostBudget)
   629  				errs = append(errs, celErrs...)
   630  				if len(errs) == 0 {
   631  					t.Errorf("missing error for %v", failingObject.object)
   632  				} else {
   633  					sawErrors := sets.NewString()
   634  					for _, err := range errs {
   635  						sawErrors.Insert(err.Error())
   636  					}
   637  					expectErrs := sets.NewString(failingObject.expectErrs...)
   638  					for _, unexpectedError := range sawErrors.Difference(expectErrs).List() {
   639  						t.Errorf("%d: unexpected error: %s", i, unexpectedError)
   640  					}
   641  					for _, missingError := range expectErrs.Difference(sawErrors).List() {
   642  						t.Errorf("%d: missing error:    %s", i, missingError)
   643  					}
   644  				}
   645  			}
   646  		})
   647  	}
   648  }
   649  
   650  func TestItemsProperty(t *testing.T) {
   651  	type args struct {
   652  		schema apiextensions.JSONSchemaProps
   653  		object interface{}
   654  	}
   655  	tests := []struct {
   656  		name    string
   657  		args    args
   658  		wantErr bool
   659  	}{
   660  		{"items in object", args{
   661  			apiextensions.JSONSchemaProps{
   662  				Properties: map[string]apiextensions.JSONSchemaProps{
   663  					"spec": {
   664  						Properties: map[string]apiextensions.JSONSchemaProps{
   665  							"replicas": {
   666  								Type: "integer",
   667  							},
   668  						},
   669  					},
   670  				},
   671  			},
   672  			map[string]interface{}{"spec": map[string]interface{}{"replicas": 1, "items": []string{"1", "2"}}},
   673  		}, false},
   674  		{"items in array", args{
   675  			apiextensions.JSONSchemaProps{
   676  				Properties: map[string]apiextensions.JSONSchemaProps{
   677  					"secrets": {
   678  						Type: "array",
   679  						Items: &apiextensions.JSONSchemaPropsOrArray{
   680  							Schema: &apiextensions.JSONSchemaProps{
   681  								Type: "string",
   682  							},
   683  						},
   684  					},
   685  				},
   686  			},
   687  			map[string]interface{}{"secrets": []string{"1", "2"}},
   688  		}, false},
   689  	}
   690  	for _, tt := range tests {
   691  		t.Run(tt.name, func(t *testing.T) {
   692  			validator, _, err := NewSchemaValidator(&tt.args.schema)
   693  			if err != nil {
   694  				t.Fatal(err)
   695  			}
   696  			if errs := ValidateCustomResource(nil, tt.args.object, validator); (len(errs) > 0) != tt.wantErr {
   697  				if len(errs) == 0 {
   698  					t.Error("expected error, but didn't get one")
   699  				} else {
   700  					t.Errorf("unexpected validation error: %v", errs)
   701  				}
   702  			}
   703  		})
   704  	}
   705  }
   706  
   707  var listMapType = "map"
   708  

View as plain text