...

Source file src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go

Documentation: k8s.io/apiextensions-apiserver/test/integration

     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 integration
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
    28  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    29  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    30  	clientschema "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
    31  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    32  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/util/wait"
    37  	"k8s.io/apimachinery/pkg/util/yaml"
    38  )
    39  
    40  func TestForProperValidationErrors(t *testing.T) {
    41  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
    42  	if err != nil {
    43  		t.Fatal(err)
    44  	}
    45  	defer tearDown()
    46  
    47  	noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.NamespaceScoped)
    48  	noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
    49  	if err != nil {
    50  		t.Fatal(err)
    51  	}
    52  
    53  	ns := "not-the-default"
    54  	noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition)
    55  
    56  	tests := []struct {
    57  		name          string
    58  		instanceFn    func() *unstructured.Unstructured
    59  		expectedError string
    60  	}{
    61  		{
    62  			name: "bad version",
    63  			instanceFn: func() *unstructured.Unstructured {
    64  				instance := fixtures.NewVersionedNoxuInstance(ns, "foo", "v2")
    65  				return instance
    66  			},
    67  			expectedError: "the API version in the data (mygroup.example.com/v2) does not match the expected API version (mygroup.example.com/v1beta1)",
    68  		},
    69  		{
    70  			name: "bad kind",
    71  			instanceFn: func() *unstructured.Unstructured {
    72  				instance := fixtures.NewNoxuInstance(ns, "foo")
    73  				instance.Object["kind"] = "SomethingElse"
    74  				return instance
    75  			},
    76  			expectedError: `SomethingElse.mygroup.example.com "foo" is invalid: kind: Invalid value: "SomethingElse": must be WishIHadChosenNoxu`,
    77  		},
    78  	}
    79  
    80  	for _, tc := range tests {
    81  		_, err := noxuResourceClient.Create(context.TODO(), tc.instanceFn(), metav1.CreateOptions{})
    82  		if err == nil {
    83  			t.Errorf("%v: expected %v", tc.name, tc.expectedError)
    84  			continue
    85  		}
    86  		// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
    87  		if !strings.Contains(err.Error(), tc.expectedError) {
    88  			t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err)
    89  			continue
    90  		}
    91  	}
    92  }
    93  
    94  func newNoxuValidationCRDs() []*apiextensionsv1.CustomResourceDefinition {
    95  	validationSchema := &apiextensionsv1.JSONSchemaProps{
    96  		Type:     "object",
    97  		Required: []string{"alpha", "beta"},
    98  		Properties: map[string]apiextensionsv1.JSONSchemaProps{
    99  			"alpha": {
   100  				Description: "Alpha is an alphanumeric string with underscores",
   101  				Type:        "string",
   102  				Pattern:     "^[a-zA-Z0-9_]*$",
   103  			},
   104  			"beta": {
   105  				Description: "Minimum value of beta is 10",
   106  				Type:        "number",
   107  				Minimum:     float64Ptr(10),
   108  			},
   109  			"gamma": {
   110  				Description: "Gamma is restricted to foo, bar and baz",
   111  				Type:        "string",
   112  				Enum: []apiextensionsv1.JSON{
   113  					{
   114  						Raw: []byte(`"foo"`),
   115  					},
   116  					{
   117  						Raw: []byte(`"bar"`),
   118  					},
   119  					{
   120  						Raw: []byte(`"baz"`),
   121  					},
   122  				},
   123  			},
   124  		},
   125  	}
   126  	validationSchemaWithDescription := validationSchema.DeepCopy()
   127  	validationSchemaWithDescription.Description = "test"
   128  	return []*apiextensionsv1.CustomResourceDefinition{
   129  		{
   130  			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
   131  			Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   132  				Group: "mygroup.example.com",
   133  				Names: apiextensionsv1.CustomResourceDefinitionNames{
   134  					Plural:     "noxus",
   135  					Singular:   "nonenglishnoxu",
   136  					Kind:       "WishIHadChosenNoxu",
   137  					ShortNames: []string{"foo", "bar", "abc", "def"},
   138  					ListKind:   "NoxuItemList",
   139  				},
   140  				Scope: apiextensionsv1.NamespaceScoped,
   141  				Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   142  					{
   143  						Name:    "v1beta1",
   144  						Served:  true,
   145  						Storage: true,
   146  						Schema: &apiextensionsv1.CustomResourceValidation{
   147  							OpenAPIV3Schema: validationSchema,
   148  						},
   149  					},
   150  					{
   151  						Name:    "v1",
   152  						Served:  true,
   153  						Storage: false,
   154  						Schema: &apiextensionsv1.CustomResourceValidation{
   155  							OpenAPIV3Schema: validationSchema,
   156  						},
   157  					},
   158  				},
   159  			},
   160  		},
   161  		{
   162  			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
   163  			Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   164  				Group: "mygroup.example.com",
   165  				Names: apiextensionsv1.CustomResourceDefinitionNames{
   166  					Plural:     "noxus",
   167  					Singular:   "nonenglishnoxu",
   168  					Kind:       "WishIHadChosenNoxu",
   169  					ShortNames: []string{"foo", "bar", "abc", "def"},
   170  					ListKind:   "NoxuItemList",
   171  				},
   172  				Scope: apiextensionsv1.NamespaceScoped,
   173  				Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   174  					{
   175  						Name:    "v1beta1",
   176  						Served:  true,
   177  						Storage: true,
   178  						Schema: &apiextensionsv1.CustomResourceValidation{
   179  							OpenAPIV3Schema: validationSchema,
   180  						},
   181  					},
   182  					{
   183  						Name:    "v1",
   184  						Served:  true,
   185  						Storage: false,
   186  						Schema: &apiextensionsv1.CustomResourceValidation{
   187  							OpenAPIV3Schema: validationSchemaWithDescription,
   188  						},
   189  					},
   190  				},
   191  			},
   192  		},
   193  	}
   194  }
   195  
   196  func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructured {
   197  	return &unstructured.Unstructured{
   198  		Object: map[string]interface{}{
   199  			"apiVersion": "mygroup.example.com/v1beta1",
   200  			"kind":       "WishIHadChosenNoxu",
   201  			"metadata": map[string]interface{}{
   202  				"namespace": namespace,
   203  				"name":      name,
   204  			},
   205  			"alpha": "foo_123",
   206  			"beta":  10,
   207  			"gamma": "bar",
   208  			"delta": "hello",
   209  		},
   210  	}
   211  }
   212  
   213  func TestCustomResourceValidation(t *testing.T) {
   214  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   215  	if err != nil {
   216  		t.Fatal(err)
   217  	}
   218  	defer tearDown()
   219  
   220  	noxuDefinitions := newNoxuValidationCRDs()
   221  	for _, noxuDefinition := range noxuDefinitions {
   222  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   223  		if err != nil {
   224  			t.Fatal(err)
   225  		}
   226  
   227  		ns := "not-the-default"
   228  		for _, v := range noxuDefinition.Spec.Versions {
   229  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   230  			instanceToCreate := newNoxuValidationInstance(ns, "foo")
   231  			instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
   232  			_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
   233  			if err != nil {
   234  				t.Fatalf("unable to create noxu instance: %v", err)
   235  			}
   236  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   237  		}
   238  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   239  			t.Fatal(err)
   240  		}
   241  	}
   242  }
   243  
   244  func TestCustomResourceItemsValidation(t *testing.T) {
   245  	tearDown, apiExtensionClient, client, err := fixtures.StartDefaultServerWithClients(t)
   246  	if err != nil {
   247  		t.Fatal(err)
   248  	}
   249  	defer tearDown()
   250  
   251  	// decode CRD manifest
   252  	obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(fixtureItemsAndType), nil, nil)
   253  	if err != nil {
   254  		t.Fatalf("failed decoding of: %v\n\n%s", err, fixtureItemsAndType)
   255  	}
   256  	crd := obj.(*apiextensionsv1.CustomResourceDefinition)
   257  
   258  	// create CRDs
   259  	t.Logf("Creating CRD %s", crd.Name)
   260  	if _, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, client); err != nil {
   261  		t.Fatalf("unexpected create error: %v", err)
   262  	}
   263  
   264  	// create CR
   265  	gvr := schema.GroupVersionResource{
   266  		Group:    crd.Spec.Group,
   267  		Version:  crd.Spec.Versions[0].Name,
   268  		Resource: crd.Spec.Names.Plural,
   269  	}
   270  	u := unstructured.Unstructured{Object: map[string]interface{}{
   271  		"apiVersion": gvr.GroupVersion().String(),
   272  		"kind":       crd.Spec.Names.Kind,
   273  		"metadata": map[string]interface{}{
   274  			"name": "foo",
   275  		},
   276  		"items-no-type": map[string]interface{}{
   277  			"items": []interface{}{
   278  				map[string]interface{}{},
   279  			},
   280  		},
   281  		"items-items-no-type": map[string]interface{}{
   282  			"items": []interface{}{
   283  				[]interface{}{map[string]interface{}{}},
   284  			},
   285  		},
   286  		"items-properties-items-no-type": map[string]interface{}{
   287  			"items": []interface{}{
   288  				map[string]interface{}{
   289  					"items": []interface{}{
   290  						map[string]interface{}{},
   291  					},
   292  				},
   293  			},
   294  		},
   295  		"type-array-no-items": map[string]interface{}{
   296  			"type": "array",
   297  		},
   298  		"items-and-type": map[string]interface{}{
   299  			"items": []interface{}{map[string]interface{}{}},
   300  			"type":  "array",
   301  		},
   302  		"issue-84880": map[string]interface{}{
   303  			"volumes": []interface{}{
   304  				map[string]interface{}{
   305  					"downwardAPI": map[string]interface{}{
   306  						"items": []interface{}{
   307  							map[string]interface{}{
   308  								"path": "annotations",
   309  							},
   310  						},
   311  					},
   312  				},
   313  			},
   314  		},
   315  	}}
   316  	_, err = client.Resource(gvr).Create(context.TODO(), &u, metav1.CreateOptions{})
   317  	if err != nil {
   318  		t.Fatalf("unexpected error: %v", err)
   319  	}
   320  }
   321  
   322  const fixtureItemsAndType = `
   323  apiVersion: apiextensions.k8s.io/v1
   324  kind: CustomResourceDefinition
   325  metadata:
   326    name: foos.tests.example.com
   327  spec:
   328    group: tests.example.com
   329    version: v1beta1
   330    names:
   331      plural: foos
   332      singular: foo
   333      kind: Foo
   334      listKind: Foolist
   335    scope: Cluster
   336    versions:
   337    - name: v1beta1
   338      served: true
   339      storage: true
   340      schema:
   341        openAPIV3Schema:
   342          type: object
   343          properties:
   344            items-no-type:
   345              type: object
   346              properties:
   347                items:
   348                  type: array
   349                  items:
   350                    type: object
   351            items-items-no-type:
   352              type: object
   353              properties:
   354                items:
   355                  type: array
   356                  items:
   357                    type: array
   358                    items:
   359                      type: object
   360            items-properties-items-no-type:
   361              type: object
   362              properties:
   363                items:
   364                  type: array
   365                  items:
   366                    type: object
   367                    properties:
   368                      items:
   369                        type: array
   370                        items:
   371                          type: object
   372            type-array-no-items:
   373              type: object
   374              properties:
   375                type:
   376                  type: string
   377            items-and-type:
   378              type: object
   379              properties:
   380                type:
   381                  type: string
   382                items:
   383                  type: array
   384                  items:
   385                    type: object
   386            default-with-items-and-no-type:
   387              type: object
   388              properties:
   389                type:
   390                  type: string
   391                items:
   392                  type: array
   393                  items:
   394                    type: object
   395              default: {"items": []}
   396            issue-84880:
   397              type: object
   398              properties:
   399                volumes:
   400                  type: array
   401                  items:
   402                    type: object
   403                    properties:
   404                      downwardAPI:
   405                        type: object
   406                        properties:
   407                          items:
   408                            items:
   409                              properties:
   410                                path:
   411                                  type: string
   412                              required:
   413                              - path
   414                              type: object
   415                            type: array
   416  `
   417  
   418  func TestCustomResourceUpdateValidation(t *testing.T) {
   419  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   420  	if err != nil {
   421  		t.Fatal(err)
   422  	}
   423  	defer tearDown()
   424  
   425  	noxuDefinitions := newNoxuValidationCRDs()
   426  	for _, noxuDefinition := range noxuDefinitions {
   427  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   428  		if err != nil {
   429  			t.Fatal(err)
   430  		}
   431  
   432  		ns := "not-the-default"
   433  		for _, v := range noxuDefinition.Spec.Versions {
   434  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   435  			instanceToCreate := newNoxuValidationInstance(ns, "foo")
   436  			instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
   437  			_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
   438  			if err != nil {
   439  				t.Fatalf("unable to create noxu instance: %v", err)
   440  			}
   441  
   442  			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
   443  			if err != nil {
   444  				t.Fatal(err)
   445  			}
   446  
   447  			// invalidate the instance
   448  			gottenNoxuInstance.Object = map[string]interface{}{
   449  				"apiVersion": "mygroup.example.com/v1beta1",
   450  				"kind":       "WishIHadChosenNoxu",
   451  				"metadata": map[string]interface{}{
   452  					"namespace": "not-the-default",
   453  					"name":      "foo",
   454  				},
   455  				"gamma": "bar",
   456  				"delta": "hello",
   457  			}
   458  
   459  			_, err = noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
   460  			if err == nil {
   461  				t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance)
   462  			}
   463  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   464  		}
   465  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   466  			t.Fatal(err)
   467  		}
   468  	}
   469  }
   470  
   471  func TestZeroValueValidation(t *testing.T) {
   472  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   473  	if err != nil {
   474  		t.Fatal(err)
   475  	}
   476  	defer tearDown()
   477  
   478  	crdManifest := `
   479  apiVersion: apiextensions.k8s.io/v1
   480  kind: CustomResourceDefinition
   481  metadata:
   482    name: zeros.tests.example.com
   483  spec:
   484    group: tests.example.com
   485    names:
   486      plural: zeros
   487      singular: zero
   488      kind: Zero
   489      listKind: Zerolist
   490    scope: Cluster
   491    versions:
   492    - name: v1
   493      served: true
   494      storage: true
   495      schema:
   496        openAPIV3Schema:
   497          type: object
   498          properties:
   499            string:
   500              type: string
   501            string_default:
   502              type: string
   503              default: ""
   504            string_null:
   505              type: string
   506              nullable: true
   507  
   508            boolean:
   509              type: boolean
   510            boolean_default:
   511              type: boolean
   512              default: false
   513            boolean_null:
   514              type: boolean
   515              nullable: true
   516  
   517            number:
   518              type: number
   519            number_default:
   520              type: number
   521              default: 0.0
   522            number_null:
   523              type: number
   524              nullable: true
   525  
   526            integer:
   527              type: integer
   528            integer_default:
   529              type: integer
   530              default: 0
   531            integer_null:
   532              type: integer
   533              nullable: true
   534  
   535            array:
   536              type: array
   537              items:
   538                type: string
   539            array_default:
   540              type: array
   541              items:
   542                type: string
   543              default: []
   544            array_null:
   545              type: array
   546              nullable: true
   547              items:
   548                type: string
   549  
   550            object:
   551              type: object
   552              properties:
   553                a:
   554                  type: string
   555            object_default:
   556              type: object
   557              properties:
   558                a:
   559                  type: string
   560              default: {}
   561            object_null:
   562              type: object
   563              nullable: true
   564              properties:
   565                a:
   566                  type: string
   567  `
   568  
   569  	// decode CRD crdManifest
   570  	crdObj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(crdManifest), nil, nil)
   571  	if err != nil {
   572  		t.Fatalf("failed decoding of: %v\n\n%s", err, crdManifest)
   573  	}
   574  	crd := crdObj.(*apiextensionsv1.CustomResourceDefinition)
   575  	_, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
   576  	if err != nil {
   577  		t.Fatal(err)
   578  	}
   579  
   580  	crObj := &unstructured.Unstructured{
   581  		Object: map[string]interface{}{
   582  			"apiVersion": "tests.example.com/v1",
   583  			"kind":       "Zero",
   584  			"metadata":   map[string]interface{}{"name": "myzero"},
   585  
   586  			"string":       "",
   587  			"string_null":  nil,
   588  			"boolean":      false,
   589  			"boolean_null": nil,
   590  			"number":       0,
   591  			"number_null":  nil,
   592  			"integer":      0,
   593  			"integer_null": nil,
   594  			"array":        []interface{}{},
   595  			"array_null":   nil,
   596  			"object":       map[string]interface{}{},
   597  			"object_null":  nil,
   598  		},
   599  	}
   600  	zerosClient := dynamicClient.Resource(schema.GroupVersionResource{Group: "tests.example.com", Version: "v1", Resource: "zeros"})
   601  	createdCR, err := zerosClient.Create(context.TODO(), crObj, metav1.CreateOptions{})
   602  	if err != nil {
   603  		t.Fatal(err)
   604  	}
   605  
   606  	expectedCR := &unstructured.Unstructured{
   607  		Object: map[string]interface{}{
   608  			"apiVersion": "tests.example.com/v1",
   609  			"kind":       "Zero",
   610  			"metadata":   createdCR.Object["metadata"],
   611  
   612  			"string":       "",
   613  			"string_null":  nil,
   614  			"boolean":      false,
   615  			"boolean_null": nil,
   616  			"number":       int64(0),
   617  			"number_null":  nil,
   618  			"integer":      int64(0),
   619  			"integer_null": nil,
   620  			"array":        []interface{}{},
   621  			"array_null":   nil,
   622  			"object":       map[string]interface{}{},
   623  			"object_null":  nil,
   624  
   625  			"string_default":  "",
   626  			"boolean_default": false,
   627  			"number_default":  int64(0),
   628  			"integer_default": int64(0),
   629  			"array_default":   []interface{}{},
   630  			"object_default":  map[string]interface{}{},
   631  		},
   632  	}
   633  
   634  	if diff := cmp.Diff(createdCR, expectedCR); len(diff) > 0 {
   635  		t.Error(diff)
   636  	}
   637  }
   638  
   639  func TestCustomResourceValidationErrors(t *testing.T) {
   640  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   641  	if err != nil {
   642  		t.Fatal(err)
   643  	}
   644  	defer tearDown()
   645  
   646  	noxuDefinitions := newNoxuValidationCRDs()
   647  	for _, noxuDefinition := range noxuDefinitions {
   648  		noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   649  		if err != nil {
   650  			t.Fatal(err)
   651  		}
   652  
   653  		ns := "not-the-default"
   654  
   655  		tests := []struct {
   656  			name           string
   657  			instanceFn     func() *unstructured.Unstructured
   658  			expectedErrors []string
   659  		}{
   660  			{
   661  				name: "bad alpha",
   662  				instanceFn: func() *unstructured.Unstructured {
   663  					instance := newNoxuValidationInstance(ns, "foo")
   664  					instance.Object["alpha"] = "foo_123!"
   665  					return instance
   666  				},
   667  				expectedErrors: []string{"alpha in body should match '^[a-zA-Z0-9_]*$'"},
   668  			},
   669  			{
   670  				name: "bad beta",
   671  				instanceFn: func() *unstructured.Unstructured {
   672  					instance := newNoxuValidationInstance(ns, "foo")
   673  					instance.Object["beta"] = 5
   674  					return instance
   675  				},
   676  				expectedErrors: []string{"beta in body should be greater than or equal to 10"},
   677  			},
   678  			{
   679  				name: "bad gamma",
   680  				instanceFn: func() *unstructured.Unstructured {
   681  					instance := newNoxuValidationInstance(ns, "foo")
   682  					instance.Object["gamma"] = "qux"
   683  					return instance
   684  				},
   685  				expectedErrors: []string{`gamma: Unsupported value: "qux": supported values: "foo", "bar", "baz"`},
   686  			},
   687  			{
   688  				name: "absent alpha and beta",
   689  				instanceFn: func() *unstructured.Unstructured {
   690  					instance := newNoxuValidationInstance(ns, "foo")
   691  					instance.Object = map[string]interface{}{
   692  						"apiVersion": "mygroup.example.com/v1beta1",
   693  						"kind":       "WishIHadChosenNoxu",
   694  						"metadata": map[string]interface{}{
   695  							"namespace": "not-the-default",
   696  							"name":      "foo",
   697  						},
   698  						"gamma": "bar",
   699  						"delta": "hello",
   700  					}
   701  					return instance
   702  				},
   703  				expectedErrors: []string{"alpha: Required value", "beta: Required value"},
   704  			},
   705  		}
   706  
   707  		for _, tc := range tests {
   708  			for _, v := range noxuDefinition.Spec.Versions {
   709  				noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   710  				instanceToCreate := tc.instanceFn()
   711  				instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
   712  				_, err := noxuResourceClient.Create(context.TODO(), instanceToCreate, metav1.CreateOptions{})
   713  				if err == nil {
   714  					t.Errorf("%v: expected %v", tc.name, tc.expectedErrors)
   715  					continue
   716  				}
   717  				// this only works when status errors contain the expect kind and version, so this effectively tests serializations too
   718  				for _, expectedError := range tc.expectedErrors {
   719  					if !strings.Contains(err.Error(), expectedError) {
   720  						t.Errorf("%v: expected %v, got %v", tc.name, expectedError, err)
   721  					}
   722  				}
   723  			}
   724  		}
   725  		if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   726  			t.Fatal(err)
   727  		}
   728  	}
   729  }
   730  
   731  func TestCRValidationOnCRDUpdate(t *testing.T) {
   732  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   733  	if err != nil {
   734  		t.Fatal(err)
   735  	}
   736  	defer tearDown()
   737  
   738  	noxuDefinitions := newNoxuValidationCRDs()
   739  	for i, noxuDefinition := range noxuDefinitions {
   740  		for _, v := range noxuDefinition.Spec.Versions {
   741  			// Re-define the CRD to make sure we start with a clean CRD
   742  			noxuDefinition := newNoxuValidationCRDs()[i]
   743  			validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
   744  			if err != nil {
   745  				t.Fatal(err)
   746  			}
   747  
   748  			// set stricter schema
   749  			validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta", "gamma"}
   750  
   751  			noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   752  			if err != nil {
   753  				t.Fatal(err)
   754  			}
   755  			ns := "not-the-default"
   756  			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
   757  			instanceToCreate := newNoxuValidationInstance(ns, "foo")
   758  			unstructured.RemoveNestedField(instanceToCreate.Object, "gamma")
   759  			instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name)
   760  
   761  			// CR is rejected
   762  			_, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name)
   763  			if err == nil {
   764  				t.Fatalf("unexpected non-error: CR should be rejected")
   765  			}
   766  
   767  			// update the CRD to a less stricter schema
   768  			_, err = UpdateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1.CustomResourceDefinition) {
   769  				validationSchema, err := getSchemaForVersion(crd, v.Name)
   770  				if err != nil {
   771  					t.Fatal(err)
   772  				}
   773  				validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta"}
   774  			})
   775  			if err != nil {
   776  				t.Fatal(err)
   777  			}
   778  
   779  			// CR is now accepted
   780  			err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   781  				_, err := noxuResourceClient.Create(context.TODO(), instanceToCreate, metav1.CreateOptions{})
   782  				if _, isStatus := err.(*apierrors.StatusError); isStatus {
   783  					if apierrors.IsInvalid(err) {
   784  						return false, nil
   785  					}
   786  				}
   787  				if err != nil {
   788  					return false, err
   789  				}
   790  				return true, nil
   791  			})
   792  			if err != nil {
   793  				t.Fatal(err)
   794  			}
   795  			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
   796  			if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   797  				t.Fatal(err)
   798  			}
   799  		}
   800  	}
   801  }
   802  
   803  func TestForbiddenFieldsInSchema(t *testing.T) {
   804  	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
   805  	if err != nil {
   806  		t.Fatal(err)
   807  	}
   808  	defer tearDown()
   809  
   810  	noxuDefinitions := newNoxuValidationCRDs()
   811  	for i, noxuDefinition := range noxuDefinitions {
   812  		for _, v := range noxuDefinition.Spec.Versions {
   813  			// Re-define the CRD to make sure we start with a clean CRD
   814  			noxuDefinition := newNoxuValidationCRDs()[i]
   815  			validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
   816  			if err != nil {
   817  				t.Fatal(err)
   818  			}
   819  			existingProperties := validationSchema.OpenAPIV3Schema.Properties
   820  			validationSchema.OpenAPIV3Schema.Properties = nil
   821  			validationSchema.OpenAPIV3Schema.AdditionalProperties = &apiextensionsv1.JSONSchemaPropsOrBool{Allows: false}
   822  			_, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   823  			if err == nil {
   824  				t.Fatalf("unexpected non-error: additionalProperties cannot be set to false")
   825  			}
   826  			// reset
   827  			validationSchema.OpenAPIV3Schema.Properties = existingProperties
   828  			validationSchema.OpenAPIV3Schema.AdditionalProperties = nil
   829  
   830  			validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1.JSONSchemaProps{
   831  				Type:        "array",
   832  				UniqueItems: true,
   833  				AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{
   834  					Allows: true,
   835  				},
   836  			}
   837  			_, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   838  			if err == nil {
   839  				t.Fatalf("unexpected non-error: uniqueItems cannot be set to true")
   840  			}
   841  
   842  			validationSchema.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta")
   843  			validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1.JSONSchemaProps{
   844  				Type:        "array",
   845  				UniqueItems: false,
   846  				Items: &apiextensionsv1.JSONSchemaPropsOrArray{
   847  					Schema: &apiextensionsv1.JSONSchemaProps{Type: "object"},
   848  				},
   849  			}
   850  
   851  			_, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   852  			if err == nil {
   853  				t.Fatal("unexpected non-error: $ref cannot be non-empty string")
   854  			}
   855  
   856  			validationSchema.OpenAPIV3Schema.Ref = nil
   857  
   858  			noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
   859  			if err != nil {
   860  				t.Fatal(err)
   861  			}
   862  			if err := fixtures.DeleteV1CustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
   863  				t.Fatal(err)
   864  			}
   865  		}
   866  	}
   867  }
   868  
   869  func TestNonStructuralSchemaConditionUpdate(t *testing.T) {
   870  	tearDown, apiExtensionClient, dynamicClient, etcdclient, etcdStoragePrefix, err := fixtures.StartDefaultServerWithClientsAndEtcd(t)
   871  	if err != nil {
   872  		t.Fatal(err)
   873  	}
   874  	defer tearDown()
   875  
   876  	manifest := `
   877  apiVersion: apiextensions.k8s.io/v1beta1
   878  kind: CustomResourceDefinition
   879  metadata:
   880    name: foos.tests.example.com
   881  spec:
   882    group: tests.example.com
   883    version: v1beta1
   884    names:
   885      plural: foos
   886      singular: foo
   887      kind: Foo
   888      listKind: Foolist
   889    scope: Namespaced
   890    validation:
   891      openAPIV3Schema:
   892        type: object
   893        properties:
   894          a: {}
   895    versions:
   896    - name: v1beta1
   897      served: true
   898      storage: true
   899  `
   900  
   901  	// decode CRD manifest
   902  	obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil)
   903  	if err != nil {
   904  		t.Fatalf("failed decoding of: %v\n\n%s", err, manifest)
   905  	}
   906  	betaCRD := obj.(*apiextensionsv1beta1.CustomResourceDefinition)
   907  	name := betaCRD.Name
   908  
   909  	// save schema for later
   910  	origSchema := &apiextensionsv1.JSONSchemaProps{
   911  		Type: "object",
   912  		Properties: map[string]apiextensionsv1.JSONSchemaProps{
   913  			"a": {
   914  				Type: "object",
   915  			},
   916  		},
   917  	}
   918  
   919  	// create CRDs.  We cannot create these in v1, but they can exist in upgraded clusters
   920  	t.Logf("Creating CRD %s", betaCRD.Name)
   921  	if _, err := fixtures.CreateCRDUsingRemovedAPI(etcdclient, etcdStoragePrefix, betaCRD, apiExtensionClient, dynamicClient); err != nil {
   922  		t.Fatal(err)
   923  	}
   924  
   925  	// wait for condition with violations
   926  	t.Log("Waiting for NonStructuralSchema condition")
   927  	var cond *apiextensionsv1.CustomResourceDefinitionCondition
   928  	err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
   929  		obj, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
   930  		if err != nil {
   931  			return false, err
   932  		}
   933  		cond = findCRDCondition(obj, apiextensionsv1.NonStructuralSchema)
   934  		return cond != nil, nil
   935  	})
   936  	if err != nil {
   937  		t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond)
   938  	}
   939  	if v := "spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) {
   940  		t.Fatalf("expected violation %q, but got: %v", v, cond.Message)
   941  	}
   942  	if v := "spec.preserveUnknownFields: Invalid value: true: must be false"; !strings.Contains(cond.Message, v) {
   943  		t.Fatalf("expected violation %q, but got: %v", v, cond.Message)
   944  	}
   945  
   946  	t.Log("fix schema")
   947  	for retry := 0; retry < 5; retry++ {
   948  		crd, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
   949  		if err != nil {
   950  			t.Fatal(err)
   951  		}
   952  		crd.Spec.Versions[0].Schema = fixtures.AllowAllSchema()
   953  		crd.Spec.PreserveUnknownFields = false
   954  		_, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
   955  		if apierrors.IsConflict(err) {
   956  			continue
   957  		}
   958  		if err != nil {
   959  			t.Fatal(err)
   960  		}
   961  		break
   962  	}
   963  
   964  	// wait for condition to go away
   965  	t.Log("Wait for condition to disappear")
   966  	err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
   967  		obj, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
   968  		if err != nil {
   969  			return false, err
   970  		}
   971  		cond = findCRDCondition(obj, apiextensionsv1.NonStructuralSchema)
   972  		return cond == nil, nil
   973  	})
   974  	if err != nil {
   975  		t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond)
   976  	}
   977  
   978  	// re-add schema
   979  	t.Log("Re-add schema")
   980  	for retry := 0; retry < 5; retry++ {
   981  		crd, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
   982  		if err != nil {
   983  			t.Fatalf("unexpected get error: %v", err)
   984  		}
   985  		crd.Spec.PreserveUnknownFields = true
   986  		crd.Spec.Versions[0].Schema = &apiextensionsv1.CustomResourceValidation{OpenAPIV3Schema: origSchema}
   987  		if _, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}); apierrors.IsConflict(err) {
   988  			continue
   989  		}
   990  		if err == nil {
   991  			t.Fatalf("missing error")
   992  		}
   993  		if !strings.Contains(err.Error(), "spec.preserveUnknownFields") {
   994  			t.Fatal(err)
   995  		}
   996  		break
   997  	}
   998  }
   999  
  1000  func TestNonStructuralSchemaConditionForCRDV1Beta1MigratedData(t *testing.T) {
  1001  	tearDown, apiExtensionClient, _, etcdClient, etcdPrefix, err := fixtures.StartDefaultServerWithClientsAndEtcd(t)
  1002  	if err != nil {
  1003  		t.Fatal(err)
  1004  	}
  1005  	defer tearDown()
  1006  
  1007  	tmpl := `
  1008  apiVersion: apiextensions.k8s.io/v1beta1
  1009  kind: CustomResourceDefinition
  1010  spec:
  1011    preserveUnknownFields: PRESERVE_UNKNOWN_FIELDS
  1012    version: v1beta1
  1013    names:
  1014      plural: foos
  1015      singular: foo
  1016      kind: Foo
  1017      listKind: Foolist
  1018    scope: Namespaced
  1019    validation: GLOBAL_SCHEMA
  1020    versions:
  1021    - name: v1beta1
  1022      served: true
  1023      storage: true
  1024      schema: V1BETA1_SCHEMA
  1025    - name: v1
  1026      served: true
  1027      schema: V1_SCHEMA
  1028  `
  1029  
  1030  	type Test struct {
  1031  		desc                                  string
  1032  		preserveUnknownFields                 string
  1033  		globalSchema, v1Schema, v1beta1Schema string
  1034  		expectedViolations                    []string
  1035  		unexpectedViolations                  []string
  1036  	}
  1037  	tests := []Test{
  1038  		{
  1039  			desc: "empty",
  1040  			expectedViolations: []string{
  1041  				"spec.preserveUnknownFields: Invalid value: true: must be false",
  1042  			},
  1043  		},
  1044  		{
  1045  			desc:                  "preserve unknown fields is false",
  1046  			preserveUnknownFields: "false",
  1047  			globalSchema: `
  1048  type: object
  1049  `,
  1050  		},
  1051  		{
  1052  			desc:                  "embedded-resource without preserve-unknown-fields, but properties",
  1053  			preserveUnknownFields: "false",
  1054  			globalSchema: `
  1055  type: object
  1056  x-kubernetes-embedded-resource: true
  1057  properties:
  1058   apiVersion:
  1059     type: string
  1060   kind:
  1061     type: string
  1062   metadata:
  1063     type: object
  1064  `,
  1065  		},
  1066  		{
  1067  			desc:                  "embedded-resource with preserve-unknown-fields",
  1068  			preserveUnknownFields: "false",
  1069  			globalSchema: `
  1070  type: object
  1071  x-kubernetes-embedded-resource: true
  1072  x-kubernetes-preserve-unknown-fields: true
  1073  `,
  1074  		},
  1075  		{
  1076  			desc: "no top-level type",
  1077  			globalSchema: `
  1078  type: ""
  1079  `,
  1080  			expectedViolations: []string{
  1081  				"spec.versions[0].schema.openAPIV3Schema.type: Required value: must not be empty at the root",
  1082  			},
  1083  		},
  1084  		{
  1085  			desc: "non-object top-level type",
  1086  			globalSchema: `
  1087  type: "integer"
  1088  `,
  1089  			expectedViolations: []string{
  1090  				"spec.versions[0].schema.openAPIV3Schema.type: Invalid value: \"integer\": must be object at the root",
  1091  			},
  1092  		},
  1093  		{
  1094  			desc: "forbidden in nested value validation",
  1095  			globalSchema: `
  1096  type: object
  1097  properties:
  1098   foo:
  1099     type: string
  1100  not:
  1101   type: string
  1102   additionalProperties: true
  1103   title: hello
  1104   description: world
  1105   nullable: true
  1106  allOf:
  1107  - properties:
  1108     foo:
  1109       type: string
  1110       additionalProperties: true
  1111       title: hello
  1112       description: world
  1113       nullable: true
  1114  anyOf:
  1115  - items:
  1116     type: string
  1117     additionalProperties: true
  1118     title: hello
  1119     description: world
  1120     nullable: true
  1121  oneOf:
  1122  - properties:
  1123     foo:
  1124       type: string
  1125       additionalProperties: true
  1126       title: hello
  1127       description: world
  1128       nullable: true
  1129  `,
  1130  			expectedViolations: []string{
  1131  				"spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.type: Forbidden: must be empty to be structural",
  1132  				"spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.additionalProperties: Forbidden: must be undefined to be structural",
  1133  				"spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.title: Forbidden: must be empty to be structural",
  1134  				"spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.description: Forbidden: must be empty to be structural",
  1135  				"spec.versions[0].schema.openAPIV3Schema.anyOf[0].items.nullable: Forbidden: must be false to be structural",
  1136  				"spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].type: Forbidden: must be empty to be structural",
  1137  				"spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural",
  1138  				"spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].title: Forbidden: must be empty to be structural",
  1139  				"spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].description: Forbidden: must be empty to be structural",
  1140  				"spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[foo].nullable: Forbidden: must be false to be structural",
  1141  				"spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].type: Forbidden: must be empty to be structural",
  1142  				"spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural",
  1143  				"spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].title: Forbidden: must be empty to be structural",
  1144  				"spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].description: Forbidden: must be empty to be structural",
  1145  				"spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[foo].nullable: Forbidden: must be false to be structural",
  1146  				"spec.versions[0].schema.openAPIV3Schema.not.type: Forbidden: must be empty to be structural",
  1147  				"spec.versions[0].schema.openAPIV3Schema.not.additionalProperties: Forbidden: must be undefined to be structural",
  1148  				"spec.versions[0].schema.openAPIV3Schema.not.title: Forbidden: must be empty to be structural",
  1149  				"spec.versions[0].schema.openAPIV3Schema.not.description: Forbidden: must be empty to be structural",
  1150  				"spec.versions[0].schema.openAPIV3Schema.not.nullable: Forbidden: must be false to be structural",
  1151  				"spec.versions[0].schema.openAPIV3Schema.items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.anyOf[0].items",
  1152  			},
  1153  			unexpectedViolations: []string{
  1154  				"spec.versions[0].schema.openAPIV3Schema.not.default",
  1155  			},
  1156  		},
  1157  		{
  1158  			desc: "invalid regex pattern",
  1159  			globalSchema: `
  1160  type: object
  1161  properties:
  1162   foo:
  1163     type: string
  1164     pattern: "+"
  1165  `,
  1166  			expectedViolations: []string{
  1167  				"spec.versions[0].schema.openAPIV3Schema.properties[foo].pattern: Invalid value: \"+\": must be a valid regular expression, but isn't: error parsing regexp: missing argument to repetition operator: `+`",
  1168  			},
  1169  		},
  1170  		{
  1171  			desc: "missing types without extensions",
  1172  			globalSchema: `
  1173  properties:
  1174   foo:
  1175     properties:
  1176       a: {}
  1177   bar:
  1178     items:
  1179       additionalProperties:
  1180         properties:
  1181           a: {}
  1182         items: {}
  1183   abc:
  1184     additionalProperties:
  1185       properties:
  1186         a:
  1187           items:
  1188             additionalProperties:
  1189               items:
  1190  `,
  1191  			expectedViolations: []string{
  1192  				"spec.versions[0].schema.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
  1193  				"spec.versions[0].schema.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
  1194  				"spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields",
  1195  				"spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items",
  1196  				"spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
  1197  				"spec.versions[0].schema.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields",
  1198  				"spec.versions[0].schema.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields",
  1199  				"spec.versions[0].schema.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items",
  1200  				"spec.versions[0].schema.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
  1201  				"spec.versions[0].schema.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields",
  1202  				"spec.versions[0].schema.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items",
  1203  				"spec.versions[0].schema.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields",
  1204  				"spec.versions[0].schema.openAPIV3Schema.type: Required value: must not be empty at the root",
  1205  			},
  1206  		},
  1207  		{
  1208  			desc: "forbidden additionalProperties at the root",
  1209  			globalSchema: `
  1210  type: object
  1211  additionalProperties: false
  1212  `,
  1213  			expectedViolations: []string{
  1214  				"spec.versions[0].schema.openAPIV3Schema.additionalProperties: Forbidden: must not be used at the root",
  1215  			},
  1216  		},
  1217  		{
  1218  			desc: "structural incomplete",
  1219  			globalSchema: `
  1220  type: object
  1221  properties:
  1222   b:
  1223     type: object
  1224     properties:
  1225       b:
  1226         type: array
  1227   c:
  1228     type: array
  1229     items:
  1230       type: object
  1231   d:
  1232     type: array
  1233  not:
  1234   properties:
  1235     a: {}
  1236     b:
  1237       not:
  1238         properties:
  1239           a: {}
  1240           b:
  1241             items: {}
  1242     c:
  1243       items:
  1244         not:
  1245           items:
  1246             properties:
  1247               a: {}
  1248     d:
  1249       items: {}
  1250  allOf:
  1251  - properties:
  1252     e: {}
  1253  anyOf:
  1254  - properties:
  1255     f: {}
  1256  oneOf:
  1257  - properties:
  1258     g: {}
  1259  `,
  1260  			expectedViolations: []string{
  1261  				"spec.versions[0].schema.openAPIV3Schema.properties[d].items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[d].items",
  1262  				"spec.versions[0].schema.openAPIV3Schema.properties[a]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[a]",
  1263  				"spec.versions[0].schema.openAPIV3Schema.properties[b].properties[a]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b].not.properties[a]",
  1264  				"spec.versions[0].schema.openAPIV3Schema.properties[b].properties[b].items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b].not.properties[b].items",
  1265  				"spec.versions[0].schema.openAPIV3Schema.properties[c].items.items: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[c].items.not.items",
  1266  				"spec.versions[0].schema.openAPIV3Schema.properties[e]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[e]",
  1267  				"spec.versions[0].schema.openAPIV3Schema.properties[f]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.anyOf[0].properties[f]",
  1268  				"spec.versions[0].schema.openAPIV3Schema.properties[g]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[g]",
  1269  			},
  1270  		},
  1271  		{
  1272  			desc:                  "structural complete",
  1273  			preserveUnknownFields: "false",
  1274  			globalSchema: `
  1275  type: object
  1276  properties:
  1277   a:
  1278     type: string
  1279   b:
  1280     type: object
  1281     properties:
  1282       a:
  1283         type: string
  1284       b:
  1285         type: array
  1286         items:
  1287           type: string
  1288   c:
  1289     type: array
  1290     items:
  1291       type: array
  1292       items:
  1293         type: object
  1294         properties:
  1295           a:
  1296             type: string
  1297   d:
  1298     type: array
  1299     items:
  1300       type: string
  1301   e:
  1302     type: string
  1303   f:
  1304     type: string
  1305   g:
  1306     type: string
  1307  not:
  1308   properties:
  1309     a: {}
  1310     b:
  1311       not:
  1312         properties:
  1313           a: {}
  1314           b:
  1315             items: {}
  1316     c:
  1317       items:
  1318         not:
  1319           items:
  1320             properties:
  1321               a: {}
  1322     d:
  1323       items: {}
  1324  allOf:
  1325  - properties:
  1326     e: {}
  1327  anyOf:
  1328  - properties:
  1329     f: {}
  1330  oneOf:
  1331  - properties:
  1332     g: {}
  1333  `,
  1334  		},
  1335  		{
  1336  			desc: "invalid v1beta1 schema",
  1337  			v1beta1Schema: `
  1338  type: object
  1339  properties:
  1340   a: {}
  1341  not:
  1342   properties:
  1343     b: {}
  1344  `,
  1345  			v1Schema: `
  1346  type: object
  1347  properties:
  1348   a:
  1349     type: string
  1350  `,
  1351  			expectedViolations: []string{
  1352  				"spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields",
  1353  				"spec.versions[0].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b]",
  1354  			},
  1355  		},
  1356  		{
  1357  			desc: "invalid v1beta1 and v1 schemas",
  1358  			v1beta1Schema: `
  1359  type: object
  1360  properties:
  1361   a: {}
  1362  not:
  1363   properties:
  1364     b: {}
  1365  `,
  1366  			v1Schema: `
  1367  type: object
  1368  properties:
  1369   c: {}
  1370  not:
  1371   properties:
  1372     d: {}
  1373  `,
  1374  			expectedViolations: []string{
  1375  				"spec.versions[0].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields",
  1376  				"spec.versions[0].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.versions[0].schema.openAPIV3Schema.not.properties[b]",
  1377  				"spec.versions[1].schema.openAPIV3Schema.properties[c].type: Required value: must not be empty for specified object fields",
  1378  				"spec.versions[1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.versions[1].schema.openAPIV3Schema.not.properties[d]",
  1379  			},
  1380  		},
  1381  		{
  1382  			desc: "metadata with non-properties",
  1383  			globalSchema: `
  1384  type: object
  1385  properties:
  1386   metadata:
  1387     minimum: 42.0
  1388  `,
  1389  			expectedViolations: []string{
  1390  				"spec.versions[0].schema.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified",
  1391  				"spec.versions[0].schema.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields",
  1392  			},
  1393  		},
  1394  		{
  1395  			desc: "metadata with other properties",
  1396  			globalSchema: `
  1397  type: object
  1398  properties:
  1399   metadata:
  1400     properties:
  1401       name:
  1402         pattern: "^[a-z]+$"
  1403       labels:
  1404         type: object
  1405         maxLength: 4
  1406  `,
  1407  			expectedViolations: []string{
  1408  				"spec.versions[0].schema.openAPIV3Schema.properties[metadata]: Forbidden: must not specify anything other than name and generateName, but metadata is implicitly specified",
  1409  				"spec.versions[0].schema.openAPIV3Schema.properties[metadata].type: Required value: must not be empty for specified object fields",
  1410  				"spec.versions[0].schema.openAPIV3Schema.properties[metadata].properties[name].type: Required value: must not be empty for specified object fields",
  1411  			},
  1412  		},
  1413  		{
  1414  			desc:                  "metadata with name property",
  1415  			preserveUnknownFields: "false",
  1416  			globalSchema: `
  1417  type: object
  1418  properties:
  1419   metadata:
  1420     type: object
  1421     properties:
  1422       name:
  1423         type: string
  1424         pattern: "^[a-z]+$"
  1425  `,
  1426  		},
  1427  		{
  1428  			desc:                  "metadata with generateName property",
  1429  			preserveUnknownFields: "false",
  1430  			globalSchema: `
  1431  type: object
  1432  properties:
  1433   metadata:
  1434     type: object
  1435     properties:
  1436       generateName:
  1437         type: string
  1438         pattern: "^[a-z]+$"
  1439  `,
  1440  		},
  1441  		{
  1442  			desc:                  "metadata with name and generateName property",
  1443  			preserveUnknownFields: "false",
  1444  			globalSchema: `
  1445  type: object
  1446  properties:
  1447   metadata:
  1448     type: object
  1449     properties:
  1450       name:
  1451         type: string
  1452         pattern: "^[a-z]+$"
  1453       generateName:
  1454         type: string
  1455         pattern: "^[a-z]+$"
  1456  `,
  1457  		},
  1458  		{
  1459  			desc: "metadata under junctors",
  1460  			globalSchema: `
  1461  type: object
  1462  properties:
  1463   metadata:
  1464     type: object
  1465     properties:
  1466       name:
  1467         type: string
  1468         pattern: "^[a-z]+$"
  1469  allOf:
  1470  - properties:
  1471     metadata: {}
  1472  anyOf:
  1473  - properties:
  1474     metadata: {}
  1475  oneOf:
  1476  - properties:
  1477     metadata: {}
  1478  not:
  1479   properties:
  1480     metadata: {}
  1481  `,
  1482  			expectedViolations: []string{
  1483  				"spec.versions[0].schema.openAPIV3Schema.anyOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
  1484  				"spec.versions[0].schema.openAPIV3Schema.allOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
  1485  				"spec.versions[0].schema.openAPIV3Schema.oneOf[0].properties[metadata]: Forbidden: must not be specified in a nested context",
  1486  				"spec.versions[0].schema.openAPIV3Schema.not.properties[metadata]: Forbidden: must not be specified in a nested context",
  1487  			},
  1488  		},
  1489  		{
  1490  			desc: "missing items for array",
  1491  			globalSchema: `
  1492  type: object
  1493  properties:
  1494   slice:
  1495     type: array
  1496  `,
  1497  			expectedViolations: []string{
  1498  				"spec.versions[0].schema.openAPIV3Schema.properties[slice].items: Required value: must be specified",
  1499  			},
  1500  		},
  1501  	}
  1502  
  1503  	for i := range tests {
  1504  		tst := tests[i]
  1505  		t.Run(tst.desc, func(t *testing.T) {
  1506  			// plug in schemas
  1507  			manifest := strings.NewReplacer(
  1508  				"GLOBAL_SCHEMA", toValidationJSON(tst.globalSchema),
  1509  				"V1BETA1_SCHEMA", toValidationJSON(tst.v1beta1Schema),
  1510  				"V1_SCHEMA", toValidationJSON(tst.v1Schema),
  1511  				"PRESERVE_UNKNOWN_FIELDS", tst.preserveUnknownFields,
  1512  			).Replace(tmpl)
  1513  
  1514  			// decode CRD manifest
  1515  			obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil)
  1516  			if err != nil {
  1517  				t.Fatalf("failed decoding of: %v\n\n%s", err, manifest)
  1518  			}
  1519  			betaCRD := obj.(*apiextensionsv1beta1.CustomResourceDefinition)
  1520  			betaCRD.Spec.Group = fmt.Sprintf("tests-%d.apiextension.k8s.io", i)
  1521  			betaCRD.Name = fmt.Sprintf("foos.%s", betaCRD.Spec.Group)
  1522  
  1523  			// create CRDs.  We cannot create these in v1, but they can exist in upgraded clusters
  1524  			t.Logf("Creating CRD %s", betaCRD.Name)
  1525  			if _, err := fixtures.CreateCRDUsingRemovedAPIWatchUnsafe(etcdClient, etcdPrefix, betaCRD, apiExtensionClient); err != nil {
  1526  				t.Fatal(err)
  1527  			}
  1528  
  1529  			if len(tst.expectedViolations) == 0 {
  1530  				// wait for condition to not appear
  1531  				var cond *apiextensionsv1.CustomResourceDefinitionCondition
  1532  				err := wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
  1533  					obj, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), betaCRD.Name, metav1.GetOptions{})
  1534  					if err != nil {
  1535  						return false, err
  1536  					}
  1537  					cond = findCRDCondition(obj, apiextensionsv1.NonStructuralSchema)
  1538  					if cond == nil {
  1539  						return false, nil
  1540  					}
  1541  					return true, nil
  1542  				})
  1543  				if err != wait.ErrWaitTimeout {
  1544  					t.Fatalf("expected no NonStructuralSchema condition, but got one: %v", cond)
  1545  				}
  1546  				return
  1547  			}
  1548  
  1549  			// wait for condition to appear with the given violations
  1550  			var cond *apiextensionsv1.CustomResourceDefinitionCondition
  1551  			err = wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
  1552  				obj, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), betaCRD.Name, metav1.GetOptions{})
  1553  				if err != nil {
  1554  					return false, err
  1555  				}
  1556  				cond = findCRDCondition(obj, apiextensionsv1.NonStructuralSchema)
  1557  				if cond != nil {
  1558  					return true, nil
  1559  				}
  1560  				return false, nil
  1561  			})
  1562  			if err != nil {
  1563  				t.Fatalf("unexpected error waiting for violations in NonStructuralSchema condition: %v", err)
  1564  			}
  1565  
  1566  			// check that the condition looks good
  1567  			if cond.Reason != "Violations" {
  1568  				t.Errorf("expected reason Violations, got: %v", cond.Reason)
  1569  			}
  1570  			if cond.Status != apiextensionsv1.ConditionTrue {
  1571  				t.Errorf("expected reason True, got: %v", cond.Status)
  1572  			}
  1573  
  1574  			// check that we got all violations
  1575  			t.Logf("Got violations: %q", cond.Message)
  1576  			for _, v := range tst.expectedViolations {
  1577  				if strings.Index(cond.Message, v) == -1 {
  1578  					t.Errorf("expected violation %q, but didn't get it", v)
  1579  				}
  1580  			}
  1581  			for _, v := range tst.unexpectedViolations {
  1582  				if strings.Index(cond.Message, v) != -1 {
  1583  					t.Errorf("unexpected violation %q", v)
  1584  				}
  1585  			}
  1586  		})
  1587  	}
  1588  }
  1589  
  1590  // findCRDCondition returns the condition you're looking for or nil.
  1591  func findCRDCondition(crd *apiextensionsv1.CustomResourceDefinition, conditionType apiextensionsv1.CustomResourceDefinitionConditionType) *apiextensionsv1.CustomResourceDefinitionCondition {
  1592  	for i := range crd.Status.Conditions {
  1593  		if crd.Status.Conditions[i].Type == conditionType {
  1594  			return &crd.Status.Conditions[i]
  1595  		}
  1596  	}
  1597  
  1598  	return nil
  1599  }
  1600  
  1601  func toValidationJSON(yml string) string {
  1602  	if len(yml) == 0 {
  1603  		return "null"
  1604  	}
  1605  	bs, err := yaml.ToJSON([]byte(yml))
  1606  	if err != nil {
  1607  		panic(err)
  1608  	}
  1609  	return fmt.Sprintf("{\"openAPIV3Schema\": %s}", string(bs))
  1610  }
  1611  
  1612  func float64Ptr(f float64) *float64 {
  1613  	return &f
  1614  }
  1615  
  1616  func strPtr(str string) *string {
  1617  	return &str
  1618  }
  1619  
  1620  func TestNonStructuralSchemaConditionForCRDV1(t *testing.T) {
  1621  	tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t)
  1622  	if err != nil {
  1623  		t.Fatal(err)
  1624  	}
  1625  	defer tearDown()
  1626  
  1627  	tmpl := `
  1628  apiVersion: apiextensions.k8s.io/v1beta1
  1629  kind: CustomResourceDefinition
  1630  spec:
  1631    preserveUnknownFields: PRESERVE_UNKNOWN_FIELDS
  1632    version: v1beta1
  1633    names:
  1634      plural: foos
  1635      singular: foo
  1636      kind: Foo
  1637      listKind: Foolist
  1638    scope: Namespaced
  1639    validation: GLOBAL_SCHEMA
  1640    versions:
  1641    - name: v1beta1
  1642      served: true
  1643      storage: true
  1644      schema: V1BETA1_SCHEMA
  1645    - name: v1
  1646      served: true
  1647      schema: V1_SCHEMA
  1648  `
  1649  
  1650  	type Test struct {
  1651  		desc                                  string
  1652  		globalSchema, v1Schema, v1beta1Schema string
  1653  		expectedCreateErrors                  []string
  1654  		unexpectedCreateErrors                []string
  1655  	}
  1656  	tests := []Test{
  1657  		{
  1658  			desc: "int-or-string and preserve-unknown-fields true",
  1659  			globalSchema: `
  1660  x-kubernetes-preserve-unknown-fields: true
  1661  x-kubernetes-int-or-string: true
  1662  `,
  1663  			expectedCreateErrors: []string{
  1664  				"spec.validation.openAPIV3Schema.x-kubernetes-preserve-unknown-fields: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
  1665  			},
  1666  		},
  1667  		{
  1668  			desc: "int-or-string and embedded-resource true",
  1669  			globalSchema: `
  1670  type: object
  1671  x-kubernetes-embedded-resource: true
  1672  x-kubernetes-int-or-string: true
  1673  `,
  1674  			expectedCreateErrors: []string{
  1675  				"spec.validation.openAPIV3Schema.x-kubernetes-embedded-resource: Invalid value: true: must be false if x-kubernetes-int-or-string is true",
  1676  			},
  1677  		},
  1678  		{
  1679  			desc: "embedded-resource without preserve-unknown-fields",
  1680  			globalSchema: `
  1681  type: object
  1682  x-kubernetes-embedded-resource: true
  1683  `,
  1684  			expectedCreateErrors: []string{
  1685  				"spec.validation.openAPIV3Schema.properties: Required value: must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields",
  1686  			},
  1687  		},
  1688  		{
  1689  			desc: "embedded-resource without preserve-unknown-fields, but properties",
  1690  			globalSchema: `
  1691  type: object
  1692  x-kubernetes-embedded-resource: true
  1693  properties:
  1694   apiVersion:
  1695     type: string
  1696   kind:
  1697     type: string
  1698   metadata:
  1699     type: object
  1700  `,
  1701  		},
  1702  		{
  1703  			desc: "embedded-resource with preserve-unknown-fields",
  1704  			globalSchema: `
  1705  type: object
  1706  x-kubernetes-embedded-resource: true
  1707  x-kubernetes-preserve-unknown-fields: true
  1708  `,
  1709  		},
  1710  		{
  1711  			desc: "embedded-resource with wrong type",
  1712  			globalSchema: `
  1713  type: array
  1714  x-kubernetes-embedded-resource: true
  1715  x-kubernetes-preserve-unknown-fields: true
  1716  `,
  1717  			expectedCreateErrors: []string{
  1718  				"spec.validation.openAPIV3Schema.type: Invalid value: \"array\": must be object if x-kubernetes-embedded-resource is true",
  1719  			},
  1720  		},
  1721  		{
  1722  			desc: "embedded-resource with empty type",
  1723  			globalSchema: `
  1724  type: ""
  1725  x-kubernetes-embedded-resource: true
  1726  x-kubernetes-preserve-unknown-fields: true
  1727  `,
  1728  			expectedCreateErrors: []string{
  1729  				"spec.validation.openAPIV3Schema.type: Required value: must be object if x-kubernetes-embedded-resource is true",
  1730  			},
  1731  		},
  1732  		{
  1733  			desc: "forbidden vendor extensions in nested value validation",
  1734  			globalSchema: `
  1735  type: object
  1736  properties:
  1737   int-or-string:
  1738     x-kubernetes-int-or-string: true
  1739   embedded-resource:
  1740     type: object
  1741     x-kubernetes-embedded-resource: true
  1742     x-kubernetes-preserve-unknown-fields: true
  1743  not:
  1744   properties:
  1745     int-or-string:
  1746       x-kubernetes-int-or-string: true
  1747     embedded-resource:
  1748       x-kubernetes-embedded-resource: true
  1749       x-kubernetes-preserve-unknown-fields: true
  1750  allOf:
  1751  - properties:
  1752     int-or-string:
  1753       x-kubernetes-int-or-string: true
  1754     embedded-resource:
  1755       x-kubernetes-embedded-resource: true
  1756       x-kubernetes-preserve-unknown-fields: true
  1757  anyOf:
  1758  - properties:
  1759     int-or-string:
  1760       x-kubernetes-int-or-string: true
  1761     embedded-resource:
  1762       x-kubernetes-embedded-resource: true
  1763       x-kubernetes-preserve-unknown-fields: true
  1764  oneOf:
  1765  - properties:
  1766     int-or-string:
  1767       x-kubernetes-int-or-string: true
  1768     embedded-resource:
  1769       x-kubernetes-embedded-resource: true
  1770       x-kubernetes-preserve-unknown-fields: true
  1771  `,
  1772  			expectedCreateErrors: []string{
  1773  				"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
  1774  				"spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
  1775  				"spec.validation.openAPIV3Schema.allOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
  1776  				"spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
  1777  				"spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
  1778  				"spec.validation.openAPIV3Schema.anyOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
  1779  				"spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
  1780  				"spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
  1781  				"spec.validation.openAPIV3Schema.oneOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
  1782  				"spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural",
  1783  				"spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural",
  1784  				"spec.validation.openAPIV3Schema.not.properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural",
  1785  			},
  1786  		},
  1787  		{
  1788  			desc: "missing types with extensions",
  1789  			globalSchema: `
  1790  properties:
  1791   foo:
  1792     properties:
  1793       a: {}
  1794   bar:
  1795     items:
  1796       additionalProperties:
  1797         properties:
  1798           a: {}
  1799         items: {}
  1800   abc:
  1801     additionalProperties:
  1802       properties:
  1803         a:
  1804           items:
  1805             additionalProperties:
  1806               items:
  1807   json:
  1808     x-kubernetes-preserve-unknown-fields: true
  1809     properties:
  1810       a: {}
  1811   int-or-string:
  1812     x-kubernetes-int-or-string: true
  1813     properties:
  1814       a: {}
  1815  `,
  1816  			expectedCreateErrors: []string{
  1817  				"spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields",
  1818  				"spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields",
  1819  				"spec.validation.openAPIV3Schema.properties[int-or-string].properties[a].type: Required value: must not be empty for specified object fields",
  1820  				"spec.validation.openAPIV3Schema.properties[json].properties[a].type: Required value: must not be empty for specified object fields",
  1821  				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields",
  1822  				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items",
  1823  				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
  1824  				"spec.validation.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields",
  1825  				"spec.validation.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields",
  1826  				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items",
  1827  				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields",
  1828  				"spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields",
  1829  				"spec.validation.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items",
  1830  				"spec.validation.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields",
  1831  				"spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root",
  1832  			},
  1833  		},
  1834  		{
  1835  			desc: "int-or-string variants",
  1836  			globalSchema: `
  1837  type: object
  1838  properties:
  1839   a:
  1840     x-kubernetes-int-or-string: true
  1841   b:
  1842     x-kubernetes-int-or-string: true
  1843     anyOf:
  1844     - type: integer
  1845     - type: string
  1846     allOf:
  1847     - pattern: abc
  1848   c:
  1849     x-kubernetes-int-or-string: true
  1850     allOf:
  1851     - anyOf:
  1852       - type: integer
  1853       - type: string
  1854     - pattern: abc
  1855     - pattern: abc
  1856   d:
  1857     x-kubernetes-int-or-string: true
  1858     anyOf:
  1859     - type: integer
  1860     - type: string
  1861       pattern: abc
  1862   e:
  1863     x-kubernetes-int-or-string: true
  1864     allOf:
  1865     - anyOf:
  1866       - type: integer
  1867       - type: string
  1868         pattern: abc
  1869     - pattern: abc
  1870   f:
  1871     x-kubernetes-int-or-string: true
  1872     anyOf:
  1873     - type: integer
  1874     - type: string
  1875     - pattern: abc
  1876   g:
  1877     x-kubernetes-int-or-string: true
  1878     anyOf:
  1879     - type: string
  1880     - type: integer
  1881  `,
  1882  			expectedCreateErrors: []string{
  1883  				"spec.validation.openAPIV3Schema.properties[d].anyOf[0].type: Forbidden: must be empty to be structural",
  1884  				"spec.validation.openAPIV3Schema.properties[d].anyOf[1].type: Forbidden: must be empty to be structural",
  1885  				"spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[0].type: Forbidden: must be empty to be structural",
  1886  				"spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[1].type: Forbidden: must be empty to be structural",
  1887  				"spec.validation.openAPIV3Schema.properties[f].anyOf[0].type: Forbidden: must be empty to be structural",
  1888  				"spec.validation.openAPIV3Schema.properties[f].anyOf[1].type: Forbidden: must be empty to be structural",
  1889  				"spec.validation.openAPIV3Schema.properties[g].anyOf[0].type: Forbidden: must be empty to be structural",
  1890  				"spec.validation.openAPIV3Schema.properties[g].anyOf[1].type: Forbidden: must be empty to be structural",
  1891  			},
  1892  			unexpectedCreateErrors: []string{
  1893  				"spec.validation.openAPIV3Schema.properties[a]",
  1894  				"spec.validation.openAPIV3Schema.properties[b]",
  1895  				"spec.validation.openAPIV3Schema.properties[c]",
  1896  			},
  1897  		},
  1898  		{
  1899  			desc: "structural complete",
  1900  			globalSchema: `
  1901  type: object
  1902  properties:
  1903   a:
  1904     type: string
  1905   b:
  1906     type: object
  1907     properties:
  1908       a:
  1909         type: string
  1910       b:
  1911         type: array
  1912         items:
  1913           type: string
  1914   c:
  1915     type: array
  1916     items:
  1917       type: array
  1918       items:
  1919         type: object
  1920         properties:
  1921           a:
  1922             type: string
  1923   d:
  1924     type: array
  1925     items:
  1926       type: string
  1927   e:
  1928     type: string
  1929   f:
  1930     type: string
  1931   g:
  1932     type: string
  1933  not:
  1934   properties:
  1935     a: {}
  1936     b:
  1937       not:
  1938         properties:
  1939           a: {}
  1940           b:
  1941             items: {}
  1942     c:
  1943       items:
  1944         not:
  1945           items:
  1946             properties:
  1947               a: {}
  1948     d:
  1949       items: {}
  1950  allOf:
  1951  - properties:
  1952     e: {}
  1953  anyOf:
  1954  - properties:
  1955     f: {}
  1956  oneOf:
  1957  - properties:
  1958     g: {}
  1959  `,
  1960  		},
  1961  		{
  1962  			desc: "metadata with name property",
  1963  			globalSchema: `
  1964  type: object
  1965  properties:
  1966   metadata:
  1967     type: object
  1968     properties:
  1969       name:
  1970         type: string
  1971         pattern: "^[a-z]+$"
  1972  `,
  1973  		},
  1974  		{
  1975  			desc: "metadata with generateName property",
  1976  			globalSchema: `
  1977  type: object
  1978  properties:
  1979   metadata:
  1980     type: object
  1981     properties:
  1982       generateName:
  1983         type: string
  1984         pattern: "^[a-z]+$"
  1985  `,
  1986  		},
  1987  		{
  1988  			desc: "metadata with name and generateName property",
  1989  			globalSchema: `
  1990  type: object
  1991  properties:
  1992   metadata:
  1993     type: object
  1994     properties:
  1995       name:
  1996         type: string
  1997         pattern: "^[a-z]+$"
  1998       generateName:
  1999         type: string
  2000         pattern: "^[a-z]+$"
  2001  `,
  2002  		},
  2003  		{
  2004  			desc: "items slice",
  2005  			globalSchema: `
  2006  type: object
  2007  properties:
  2008   slice:
  2009     type: array
  2010     items:
  2011     - type: string
  2012     - type: integer
  2013  `,
  2014  			expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].items: Forbidden: items must be a schema object and not an array"},
  2015  		},
  2016  		{
  2017  			desc: "items slice in value validation",
  2018  			globalSchema: `
  2019  type: object
  2020  properties:
  2021   slice:
  2022     type: array
  2023     items:
  2024       type: string
  2025     not:
  2026       items:
  2027       - type: string
  2028  `,
  2029  			expectedCreateErrors: []string{"spec.validation.openAPIV3Schema.properties[slice].not.items: Forbidden: items must be a schema object and not an array"},
  2030  		},
  2031  	}
  2032  
  2033  	for i := range tests {
  2034  		tst := tests[i]
  2035  		t.Run(tst.desc, func(t *testing.T) {
  2036  			// plug in schemas
  2037  			manifest := strings.NewReplacer(
  2038  				"GLOBAL_SCHEMA", toValidationJSON(tst.globalSchema),
  2039  				"V1BETA1_SCHEMA", toValidationJSON(tst.v1beta1Schema),
  2040  				"V1_SCHEMA", toValidationJSON(tst.v1Schema),
  2041  				"PRESERVE_UNKNOWN_FIELDS", "false",
  2042  			).Replace(tmpl)
  2043  
  2044  			// decode CRD manifest
  2045  			obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil)
  2046  			if err != nil {
  2047  				t.Fatalf("failed decoding of: %v\n\n%s", err, manifest)
  2048  			}
  2049  			betaCRD := obj.(*apiextensionsv1beta1.CustomResourceDefinition)
  2050  			betaCRD.Spec.Group = fmt.Sprintf("tests-%d.apiextension.testing-k8s.io", i)
  2051  			betaCRD.Name = fmt.Sprintf("foos.%s", betaCRD.Spec.Group)
  2052  
  2053  			internalCRD := &apiextensions.CustomResourceDefinition{}
  2054  			err = apiextensionsv1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(betaCRD, internalCRD, nil)
  2055  			if err != nil {
  2056  				t.Fatal(err)
  2057  			}
  2058  
  2059  			crd := &apiextensionsv1.CustomResourceDefinition{}
  2060  			err = apiextensionsv1.Convert_apiextensions_CustomResourceDefinition_To_v1_CustomResourceDefinition(internalCRD, crd, nil)
  2061  			if err != nil {
  2062  				t.Fatal(err)
  2063  			}
  2064  
  2065  			// create CRDs
  2066  			_, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{})
  2067  			if len(tst.expectedCreateErrors) > 0 && err == nil {
  2068  				t.Fatalf("expected create errors, got none")
  2069  			} else if len(tst.expectedCreateErrors) == 0 && err != nil {
  2070  				t.Fatalf("unexpected create error: %v", err)
  2071  			} else if err != nil {
  2072  				for _, expectedErr := range tst.expectedCreateErrors {
  2073  					if !strings.Contains(err.Error(), expectedErr) {
  2074  						t.Errorf("expected error containing '%s', got '%s'", expectedErr, err.Error())
  2075  					}
  2076  				}
  2077  				for _, unexpectedErr := range tst.unexpectedCreateErrors {
  2078  					if strings.Contains(err.Error(), unexpectedErr) {
  2079  						t.Errorf("unexpected error containing '%s': '%s'", unexpectedErr, err.Error())
  2080  					}
  2081  				}
  2082  			}
  2083  		})
  2084  	}
  2085  }
  2086  

View as plain text