1
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
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
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
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
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
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
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
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
742 noxuDefinition := newNoxuValidationCRDs()[i]
743 validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name)
744 if err != nil {
745 t.Fatal(err)
746 }
747
748
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
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
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
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
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
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
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
910 origSchema := &apiextensionsv1.JSONSchemaProps{
911 Type: "object",
912 Properties: map[string]apiextensionsv1.JSONSchemaProps{
913 "a": {
914 Type: "object",
915 },
916 },
917 }
918
919
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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