1
16
17 package apiserver
18
19 import (
20 "context"
21 "fmt"
22 "strings"
23 "testing"
24
25 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
26 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
27 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
28 "k8s.io/apiextensions-apiserver/test/integration/fixtures"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31 "k8s.io/apimachinery/pkg/runtime/schema"
32 "k8s.io/apimachinery/pkg/util/json"
33 genericfeatures "k8s.io/apiserver/pkg/features"
34 "k8s.io/apiserver/pkg/storage/names"
35 utilfeature "k8s.io/apiserver/pkg/util/feature"
36 "k8s.io/client-go/dynamic"
37 featuregatetesting "k8s.io/component-base/featuregate/testing"
38
39 apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
40 "k8s.io/kubernetes/test/integration/framework"
41 )
42
43
44
45 func TestCustomResourceValidators(t *testing.T) {
46 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, true)()
47
48 server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
49 if err != nil {
50 t.Fatal(err)
51 }
52 defer server.TearDownFn()
53 config := server.ClientConfig
54
55 apiExtensionClient, err := clientset.NewForConfig(config)
56 if err != nil {
57 t.Fatal(err)
58 }
59 dynamicClient, err := dynamic.NewForConfig(config)
60 if err != nil {
61 t.Fatal(err)
62 }
63
64 t.Run("Structural schema", func(t *testing.T) {
65 structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithValidators)
66 crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
67 if err != nil {
68 t.Fatal(err)
69 }
70 gvr := schema.GroupVersionResource{
71 Group: crd.Spec.Group,
72 Version: crd.Spec.Versions[0].Name,
73 Resource: crd.Spec.Names.Plural,
74 }
75 crClient := dynamicClient.Resource(gvr)
76
77 t.Run("CRD creation MUST allow data that is valid according to x-kubernetes-validations", func(t *testing.T) {
78 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
79 _, err = crClient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{
80 "apiVersion": gvr.Group + "/" + gvr.Version,
81 "kind": crd.Spec.Names.Kind,
82 "metadata": map[string]interface{}{
83 "name": name1,
84 },
85 "spec": map[string]interface{}{
86 "x": int64(2),
87 "y": int64(2),
88 "limit": int64(123),
89 },
90 }}, metav1.CreateOptions{})
91 if err != nil {
92 t.Errorf("Failed to create custom resource: %v", err)
93 }
94 })
95 t.Run("custom resource create and update MUST NOT allow data that is invalid according to x-kubernetes-validations if the feature gate is enabled", func(t *testing.T) {
96 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
97
98
99 cr := &unstructured.Unstructured{Object: map[string]interface{}{
100 "apiVersion": gvr.Group + "/" + gvr.Version,
101 "kind": crd.Spec.Names.Kind,
102 "metadata": map[string]interface{}{
103 "name": name1,
104 },
105 "spec": map[string]interface{}{
106 "x": int64(-1),
107 "y": int64(0),
108 },
109 }}
110
111
112 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
113 if err == nil {
114 t.Fatal("Expected create of invalid custom resource to fail")
115 } else {
116 if !strings.Contains(err.Error(), "failed rule: self.spec.x + self.spec.y") {
117 t.Fatalf("Expected error to contain %s but got %v", "failed rule: self.spec.x + self.spec.y", err.Error())
118 }
119 }
120
121
122 cr.Object["spec"] = map[string]interface{}{
123 "x": int64(2),
124 "y": int64(2),
125 "extra": "anything?",
126 "floatMap": map[string]interface{}{
127 "key1": 0.2,
128 "key2": 0.3,
129 },
130 "assocList": []interface{}{
131 map[string]interface{}{
132 "k": "a",
133 "v": "1",
134 },
135 },
136 "limit": nil,
137 }
138
139 cr, err := crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
140 if err != nil {
141 t.Fatalf("Unexpected error creating custom resource: %v", err)
142 }
143
144
145 cases := []struct {
146 name string
147 spec map[string]interface{}
148 }{
149 {
150 name: "spec vs. status default value",
151 spec: map[string]interface{}{
152 "x": 3,
153 "y": -4,
154 },
155 },
156 {
157 name: "nested string field",
158 spec: map[string]interface{}{
159 "extra": "something",
160 },
161 },
162 {
163 name: "nested array",
164 spec: map[string]interface{}{
165 "floatMap": map[string]interface{}{
166 "key1": 0.1,
167 "key2": 0.2,
168 },
169 },
170 },
171 {
172 name: "nested associative list",
173 spec: map[string]interface{}{
174 "assocList": []interface{}{
175 map[string]interface{}{
176 "k": "a",
177 "v": "2",
178 },
179 },
180 },
181 },
182 }
183 for _, tc := range cases {
184 t.Run(tc.name, func(t *testing.T) {
185 cr.Object["spec"] = tc.spec
186
187 _, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
188 if err == nil {
189 t.Fatal("Expected invalid update of custom resource to fail")
190 } else {
191 if !strings.Contains(err.Error(), "failed rule") {
192 t.Fatalf("Expected error to contain %s but got %v", "failed rule", err.Error())
193 }
194 }
195 })
196 }
197
198
199 cr.Object["status"] = map[string]interface{}{
200 "z": int64(5),
201 }
202 _, err = crClient.UpdateStatus(context.TODO(), cr, metav1.UpdateOptions{})
203 if err == nil {
204 t.Fatal("Expected invalid update of custom resource status to fail")
205 } else {
206 if !strings.Contains(err.Error(), "failed rule: self.spec.x + self.spec.y") {
207 t.Fatalf("Expected error to contain %s but got %v", "failed rule: self.spec.x + self.spec.y", err.Error())
208 }
209 }
210
211
212 cr.Object["status"] = map[string]interface{}{
213 "z": int64(3),
214 }
215
216 _, err = crClient.UpdateStatus(context.TODO(), cr, metav1.UpdateOptions{})
217 if err != nil {
218 t.Fatalf("Unexpected error updating custom resource status: %v", err)
219 }
220 })
221 })
222 t.Run("CRD writes MUST fail for a non-structural schema containing x-kubernetes-validations", func(t *testing.T) {
223
224 nonStructuralCRD, err := fixtures.CreateCRDUsingRemovedAPI(server.EtcdClient, server.EtcdStoragePrefix, nonStructuralCrdWithValidations(), apiExtensionClient, dynamicClient)
225 if err != nil {
226 t.Fatalf("Unexpected error non-structural CRD by writing directly to etcd: %v", err)
227 }
228
229 crd, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), nonStructuralCRD.Name, metav1.GetOptions{})
230 if err != nil {
231 t.Fatalf("Unexpected error: %v", err)
232 }
233 nonStructural := false
234 for _, c := range crd.Status.Conditions {
235 if c.Type == apiextensionsv1.NonStructuralSchema {
236 nonStructural = true
237 }
238 }
239 if !nonStructural {
240 t.Fatal("Expected CRD to be non-structural")
241 }
242
243
244 crd.Spec.Versions[0].Schema.OpenAPIV3Schema.XValidations = apiextensionsv1.ValidationRules{
245 {
246 Rule: "has(self.foo)",
247 },
248 }
249 _, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
250 if err == nil {
251 t.Fatal("Expected error")
252 }
253 })
254 t.Run("CRD creation MUST fail if a x-kubernetes-validations rule accesses a metadata field other than name", func(t *testing.T) {
255 structuralWithValidators := crdWithSchema(t, "InvalidStructuralMetadata", structuralSchemaWithInvalidMetadataValidators)
256 _, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
257 if err == nil {
258 t.Error("Expected error creating custom resource but got none")
259 } else if !strings.Contains(err.Error(), "undefined field 'labels'") {
260 t.Errorf("Expected error to contain %s but got %v", "undefined field 'labels'", err.Error())
261 }
262 })
263 t.Run("CRD creation MUST pass if a x-kubernetes-validations rule accesses metadata.name", func(t *testing.T) {
264 structuralWithValidators := crdWithSchema(t, "ValidStructuralMetadata", structuralSchemaWithValidMetadataValidators)
265 _, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
266 if err != nil {
267 t.Error("Unexpected error creating custom resource but metadata validation rule")
268 }
269 })
270 t.Run("CRD creation MUST pass for an CRD with empty field", func(t *testing.T) {
271 structuralWithValidators := crdWithSchema(t, "WithEmptyObject", structuralSchemaWithEmptyObject)
272 _, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
273 if err != nil {
274 t.Errorf("unexpected error creating CRD with empty field: %v", err)
275 }
276 })
277 t.Run("CR creation MUST fail if a x-kubernetes-validations rule exceeds the runtime cost limit", func(t *testing.T) {
278 structuralWithValidators := crdWithSchema(t, "RuntimeCostLimit", structuralSchemaWithCostLimit)
279 crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
280 if err != nil {
281 t.Errorf("Unexpected error creating custom resource definition: %v", err)
282 }
283 gvr := schema.GroupVersionResource{
284 Group: crd.Spec.Group,
285 Version: crd.Spec.Versions[0].Name,
286 Resource: crd.Spec.Names.Plural,
287 }
288 crClient := dynamicClient.Resource(gvr)
289 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
290 cr := &unstructured.Unstructured{Object: map[string]interface{}{
291 "apiVersion": gvr.Group + "/" + gvr.Version,
292 "kind": crd.Spec.Names.Kind,
293 "metadata": map[string]interface{}{
294 "name": name1,
295 },
296 "spec": map[string]interface{}{
297 "list": genLargeArray(725, 20),
298 },
299 }}
300 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
301 if err == nil {
302 t.Fatal("Expected error creating custom resource")
303 } else if !strings.Contains(err.Error(), "call cost exceeds limit") {
304 t.Errorf("Expected error to contain %s but got %v", "call cost exceeds limit", err.Error())
305 }
306 })
307 t.Run("Schema with valid transition rule", func(t *testing.T) {
308 structuralWithValidators := crdWithSchema(t, "ValidTransitionRule", structuralSchemaWithValidTransitionRule)
309 crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
310 if err != nil {
311 t.Fatal(err)
312 }
313 gvr := schema.GroupVersionResource{
314 Group: crd.Spec.Group,
315 Version: crd.Spec.Versions[0].Name,
316 Resource: crd.Spec.Names.Plural,
317 }
318 crClient := dynamicClient.Resource(gvr)
319
320 t.Run("custom resource update MUST pass if a x-kubernetes-validations rule contains a valid transition rule", func(t *testing.T) {
321 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
322 cr := &unstructured.Unstructured{Object: map[string]interface{}{
323 "apiVersion": gvr.Group + "/" + gvr.Version,
324 "kind": crd.Spec.Names.Kind,
325 "metadata": map[string]interface{}{
326 "name": name1,
327 },
328 "spec": map[string]interface{}{
329 "someImmutableThing": "original",
330 "somethingElse": "original",
331 },
332 }}
333 cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
334 if err != nil {
335 t.Fatalf("Unexpected error creating custom resource: %v", err)
336 }
337 cr.Object["spec"].(map[string]interface{})["somethingElse"] = "new value"
338 _, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
339 if err != nil {
340 t.Fatalf("Unexpected error updating custom resource: %v", err)
341 }
342 })
343 t.Run("custom resource update MUST fail if a x-kubernetes-validations rule contains an invalid transition rule", func(t *testing.T) {
344 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
345 cr := &unstructured.Unstructured{Object: map[string]interface{}{
346 "apiVersion": gvr.Group + "/" + gvr.Version,
347 "kind": crd.Spec.Names.Kind,
348 "metadata": map[string]interface{}{
349 "name": name1,
350 },
351 "spec": map[string]interface{}{
352 "someImmutableThing": "original",
353 "somethingElse": "original",
354 },
355 }}
356 cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
357 if err != nil {
358 t.Fatalf("Unexpected error creating custom resource: %v", err)
359 }
360 cr.Object["spec"].(map[string]interface{})["someImmutableThing"] = "new value"
361 _, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
362 if err == nil {
363 t.Fatalf("Expected error updating custom resource: %v", err)
364 } else if !strings.Contains(err.Error(), "failed rule: self.someImmutableThing == oldSelf.someImmutableThing") {
365 t.Errorf("Expected error to contain %s but got %v", "failed rule: self.someImmutableThing == oldSelf.someImmutableThing", err.Error())
366 }
367 })
368 })
369
370 t.Run("CRD creation MUST fail if a x-kubernetes-validations rule contains invalid transition rule", func(t *testing.T) {
371 structuralWithValidators := crdWithSchema(t, "InvalidTransitionRule", structuralSchemaWithInvalidTransitionRule)
372 _, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
373 if err == nil {
374 t.Error("Expected error creating custom resource but got none")
375 } else if !strings.Contains(err.Error(), "oldSelf cannot be used on the uncorrelatable portion of the schema") {
376 t.Errorf("Expected error to contain %s but got %v", "oldSelf cannot be used on the uncorrelatable portion of the schema", err.Error())
377 }
378 })
379 t.Run("Schema with default map key transition rule", func(t *testing.T) {
380 structuralWithValidators := crdWithSchema(t, "DefaultMapKeyTransitionRule", structuralSchemaWithDefaultMapKeyTransitionRule)
381 crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
382 if err != nil {
383 t.Fatal(err)
384 }
385 gvr := schema.GroupVersionResource{
386 Group: crd.Spec.Group,
387 Version: crd.Spec.Versions[0].Name,
388 Resource: crd.Spec.Names.Plural,
389 }
390 crClient := dynamicClient.Resource(gvr)
391
392 t.Run("custom resource update MUST fail if a x-kubernetes-validations if a transition rule contained in a mapList with default map keys fails validation", func(t *testing.T) {
393 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
394 cr := &unstructured.Unstructured{Object: map[string]interface{}{
395 "apiVersion": gvr.Group + "/" + gvr.Version,
396 "kind": crd.Spec.Names.Kind,
397 "metadata": map[string]interface{}{
398 "name": name1,
399 },
400 "spec": map[string]interface{}{
401 "list": []interface{}{
402 map[string]interface{}{
403 "k1": "x",
404 "v": "value",
405 },
406 },
407 },
408 }}
409 cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
410 if err != nil {
411 t.Fatalf("Unexpected error creating custom resource: %v", err)
412 }
413 item := cr.Object["spec"].(map[string]interface{})["list"].([]interface{})[0].(map[string]interface{})
414 item["k2"] = "DEFAULT"
415 item["v"] = "new value"
416 _, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
417 if err == nil {
418 t.Fatalf("Expected error updating custom resource: %v", err)
419 } else if !strings.Contains(err.Error(), "failed rule: self.v == oldSelf.v") {
420 t.Errorf("Expected error to contain %s but got %v", "failed rule: self.v == oldSelf.v", err.Error())
421 }
422 })
423 })
424 }
425
426
427
428 func TestCustomResourceValidatorsWithBlockingErrors(t *testing.T) {
429 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, true)()
430
431 server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
432 if err != nil {
433 t.Fatal(err)
434 }
435 defer server.TearDownFn()
436 config := server.ClientConfig
437
438 apiExtensionClient, err := clientset.NewForConfig(config)
439 if err != nil {
440 t.Fatal(err)
441 }
442 dynamicClient, err := dynamic.NewForConfig(config)
443 if err != nil {
444 t.Fatal(err)
445 }
446
447 t.Run("Structural schema", func(t *testing.T) {
448 structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithBlockingErr)
449 crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
450 if err != nil {
451 t.Fatal(err)
452 }
453 gvr := schema.GroupVersionResource{
454 Group: crd.Spec.Group,
455 Version: crd.Spec.Versions[0].Name,
456 Resource: crd.Spec.Names.Plural,
457 }
458 crClient := dynamicClient.Resource(gvr)
459
460 t.Run("CRD creation MUST allow data that is valid according to x-kubernetes-validations", func(t *testing.T) {
461 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
462 _, err = crClient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{
463 "apiVersion": gvr.Group + "/" + gvr.Version,
464 "kind": crd.Spec.Names.Kind,
465 "metadata": map[string]interface{}{
466 "name": name1,
467 },
468 "spec": map[string]interface{}{
469 "x": int64(2),
470 "y": int64(2),
471 "limit": int64(123),
472 },
473 }}, metav1.CreateOptions{})
474 if err != nil {
475 t.Errorf("Failed to create custom resource: %v", err)
476 }
477 })
478 t.Run("custom resource create and update MUST NOT allow data if failed validation", func(t *testing.T) {
479 name1 := names.SimpleNameGenerator.GenerateName("cr-1")
480
481
482 cr := &unstructured.Unstructured{Object: map[string]interface{}{
483 "apiVersion": gvr.Group + "/" + gvr.Version,
484 "kind": crd.Spec.Names.Kind,
485 "metadata": map[string]interface{}{
486 "name": name1,
487 },
488 "spec": map[string]interface{}{
489 "x": int64(-1),
490 "y": int64(0),
491 },
492 }}
493
494
495 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
496 if err == nil {
497 t.Fatal("Expected create of invalid custom resource to fail")
498 } else {
499 if !strings.Contains(err.Error(), "self.spec.x + self.spec.y must be greater than or equal to 0") {
500 t.Fatalf("Expected error to contain %s but got %v", "self.spec.x + self.spec.y must be greater than or equal to 0", err.Error())
501 }
502 }
503 })
504 t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxLength", func(t *testing.T) {
505 name2 := names.SimpleNameGenerator.GenerateName("cr-2")
506
507
508 cr := &unstructured.Unstructured{Object: map[string]interface{}{
509 "apiVersion": gvr.Group + "/" + gvr.Version,
510 "kind": crd.Spec.Names.Kind,
511 "metadata": map[string]interface{}{
512 "name": name2,
513 },
514 "spec": map[string]interface{}{
515 "x": int64(2),
516 "y": int64(2),
517 "extra": strings.Repeat("x", 201),
518 "floatMap": map[string]interface{}{
519 "key1": 0.2,
520 "key2": 0.3,
521 },
522 "limit": nil,
523 },
524 }}
525
526 _, err := crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
527 if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
528 t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
529 }
530 })
531 t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxItems", func(t *testing.T) {
532 name2 := names.SimpleNameGenerator.GenerateName("cr-2")
533
534 cr := &unstructured.Unstructured{Object: map[string]interface{}{
535 "apiVersion": gvr.Group + "/" + gvr.Version,
536 "kind": crd.Spec.Names.Kind,
537 "metadata": map[string]interface{}{
538 "name": name2,
539 },
540 "spec": map[string]interface{}{
541 "x": int64(2),
542 "y": int64(2),
543 "floatMap": map[string]interface{}{
544 "key1": 0.2,
545 "key2": 0.3,
546 },
547 "assocList": []interface{}{},
548 "limit": nil,
549 },
550 }}
551 assocList := cr.Object["spec"].(map[string]interface{})["assocList"].([]interface{})
552 for i := 1; i <= 101; i++ {
553 assocList = append(assocList, map[string]interface{}{
554 "k": "a",
555 "v": fmt.Sprintf("%d", i),
556 })
557 }
558 cr.Object["spec"].(map[string]interface{})["assocList"] = assocList
559
560 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
561 if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
562 t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
563 }
564 })
565 t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxProperties", func(t *testing.T) {
566 name2 := names.SimpleNameGenerator.GenerateName("cr-2")
567
568 cr := &unstructured.Unstructured{Object: map[string]interface{}{
569 "apiVersion": gvr.Group + "/" + gvr.Version,
570 "kind": crd.Spec.Names.Kind,
571 "metadata": map[string]interface{}{
572 "name": name2,
573 },
574 "spec": map[string]interface{}{
575 "x": int64(2),
576 "y": int64(2),
577 "floatMap": map[string]interface{}{},
578 "assocList": []interface{}{
579 map[string]interface{}{
580 "k": "a",
581 "v": "1",
582 },
583 },
584 "limit": nil,
585 },
586 }}
587 floatMap := cr.Object["spec"].(map[string]interface{})["floatMap"].(map[string]interface{})
588 for i := 1; i <= 101; i++ {
589 floatMap[fmt.Sprintf("key%d", i)] = float64(i) / 10
590 }
591
592 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
593 if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
594 t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
595 }
596 })
597 t.Run("custom resource create and update MUST NOT allow data if there is blocking error of missing required field", func(t *testing.T) {
598 name2 := names.SimpleNameGenerator.GenerateName("cr-2")
599
600 cr := &unstructured.Unstructured{Object: map[string]interface{}{
601 "apiVersion": gvr.Group + "/" + gvr.Version,
602 "kind": crd.Spec.Names.Kind,
603 "metadata": map[string]interface{}{
604 "name": name2,
605 },
606 "spec": map[string]interface{}{
607 "x": int64(2),
608 "y": int64(2),
609 "floatMap": map[string]interface{}{
610 "key1": 0.2,
611 "key2": 0.3,
612 },
613 "assocList": []interface{}{
614 map[string]interface{}{
615 "k": "1",
616 },
617 },
618 "limit": nil,
619 },
620 }}
621
622 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
623 if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
624 t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
625 }
626 })
627 t.Run("custom resource create and update MUST NOT allow data if there is blocking error of type", func(t *testing.T) {
628 name2 := names.SimpleNameGenerator.GenerateName("cr-2")
629
630 cr := &unstructured.Unstructured{Object: map[string]interface{}{
631 "apiVersion": gvr.Group + "/" + gvr.Version,
632 "kind": crd.Spec.Names.Kind,
633 "metadata": map[string]interface{}{
634 "name": name2,
635 },
636 "spec": map[string]interface{}{
637 "x": int64(2),
638 "y": int64(2),
639 "floatMap": map[string]interface{}{
640 "key1": 0.2,
641 "key2": 0.3,
642 },
643 "assocList": []interface{}{
644 map[string]interface{}{
645 "k": "a",
646 "v": true,
647 },
648 },
649 "limit": nil,
650 },
651 }}
652
653 _, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
654 if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
655 t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
656 }
657 })
658 })
659 }
660
661 func nonStructuralCrdWithValidations() *apiextensionsv1beta1.CustomResourceDefinition {
662 return &apiextensionsv1beta1.CustomResourceDefinition{
663 ObjectMeta: metav1.ObjectMeta{
664 Name: "foos.nonstructural.cr.bar.com",
665 },
666 Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
667 Group: "nonstructural.cr.bar.com",
668 Version: "v1",
669 Scope: apiextensionsv1beta1.NamespaceScoped,
670 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
671 Plural: "foos",
672 Kind: "Foo",
673 },
674 Validation: &apiextensionsv1beta1.CustomResourceValidation{
675 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
676 Type: "object",
677 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
678 "foo": {},
679 },
680 },
681 },
682 },
683 }
684 }
685
686 func genLargeArray(n, x int64) []int64 {
687 arr := make([]int64, n)
688 for i := int64(0); i < n; i++ {
689 arr[i] = x
690 }
691 return arr
692 }
693
694 func crdWithSchema(t *testing.T, kind string, schemaJson []byte) *apiextensionsv1.CustomResourceDefinition {
695 plural := strings.ToLower(kind) + "s"
696 var c apiextensionsv1.CustomResourceValidation
697 err := json.Unmarshal(schemaJson, &c)
698 if err != nil {
699 t.Fatal(err)
700 }
701
702 return &apiextensionsv1.CustomResourceDefinition{
703 ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s.mygroup.example.com", plural)},
704 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
705 Group: "mygroup.example.com",
706 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
707 Name: "v1beta1",
708 Served: true,
709 Storage: true,
710 Schema: &c,
711 Subresources: &apiextensionsv1.CustomResourceSubresources{
712 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
713 },
714 }},
715 Names: apiextensionsv1.CustomResourceDefinitionNames{
716 Plural: plural,
717 Kind: kind,
718 },
719 Scope: apiextensionsv1.ClusterScoped,
720 },
721 }
722 }
723
724 var structuralSchemaWithValidators = []byte(`
725 {
726 "openAPIV3Schema": {
727 "description": "CRD with CEL validators",
728 "type": "object",
729 "x-kubernetes-validations": [
730 {
731 "rule": "self.spec.x + self.spec.y >= (has(self.status) ? self.status.z : 0)"
732 }
733 ],
734 "properties": {
735 "spec": {
736 "type": "object",
737 "properties": {
738 "x": {
739 "type": "integer",
740 "default": 0
741 },
742 "y": {
743 "type": "integer",
744 "default": 0
745 },
746 "extra": {
747 "type": "string",
748 "x-kubernetes-validations": [
749 {
750 "rule": "self.startsWith('anything')"
751 }
752 ]
753 },
754 "floatMap": {
755 "type": "object",
756 "additionalProperties": { "type": "number" },
757 "x-kubernetes-validations": [
758 {
759 "rule": "self.all(k, self[k] >= 0.2)"
760 }
761 ]
762 },
763 "assocList": {
764 "type": "array",
765 "maxItems": 100,
766 "items": {
767 "type": "object",
768 "properties": {
769 "k": { "type": "string", "maxLength": 200},
770 "v": { "type": "string", "maxLength": 200}
771 },
772 "required": ["k"]
773 },
774 "x-kubernetes-list-type": "map",
775 "x-kubernetes-list-map-keys": ["k"],
776 "x-kubernetes-validations": [
777 {
778 "rule": "self.exists(e, e.k == 'a' && e.v == '1')"
779 }
780 ]
781 },
782 "limit": {
783 "nullable": true,
784 "x-kubernetes-validations": [
785 {
786 "rule": "type(self) == int && self == 123"
787 }
788 ],
789 "x-kubernetes-int-or-string": true
790 }
791 }
792 },
793 "status": {
794 "type": "object",
795 "properties": {
796 "z": {
797 "type": "integer",
798 "default": 0
799 }
800 }
801 }
802 }
803 }
804 }`)
805
806 var structuralSchemaWithBlockingErr = []byte(`
807 {
808 "openAPIV3Schema": {
809 "description": "CRD with CEL validators",
810 "type": "object",
811 "x-kubernetes-validations": [
812 {
813 "rule": "self.spec.x + self.spec.y >= (has(self.status) ? self.status.z : 0)",
814 "messageExpression": "\"self.spec.x + self.spec.y must be greater than or equal to 0\""
815 }
816 ],
817 "properties": {
818 "spec": {
819 "type": "object",
820 "properties": {
821 "x": {
822 "type": "integer",
823 "default": 0
824 },
825 "y": {
826 "type": "integer",
827 "default": 0
828 },
829 "extra": {
830 "type": "string",
831 "maxLength": 200,
832 "x-kubernetes-validations": [
833 {
834 "rule": "self.startsWith('anything')"
835 }
836 ]
837 },
838 "floatMap": {
839 "type": "object",
840 "maxProperties": 100,
841 "additionalProperties": { "type": "number" },
842 "x-kubernetes-validations": [
843 {
844 "rule": "self.all(k, self[k] >= 0.2)"
845 }
846 ]
847 },
848 "assocList": {
849 "type": "array",
850 "maxItems": 100,
851 "items": {
852 "type": "object",
853 "properties": {
854 "k": { "type": "string" },
855 "v": { "type": "string" }
856 },
857 "required": ["k", "v"]
858 },
859 "x-kubernetes-list-type": "map",
860 "x-kubernetes-list-map-keys": ["k"],
861 "x-kubernetes-validations": [
862 {
863 "rule": "self.exists(e, e.k == 'a' && e.v == '1')"
864 }
865 ]
866 },
867 "limit": {
868 "nullable": true,
869 "x-kubernetes-validations": [
870 {
871 "rule": "type(self) == int && self == 123"
872 }
873 ],
874 "x-kubernetes-int-or-string": true
875 }
876 }
877 },
878 "status": {
879 "type": "object",
880 "properties": {
881 "z": {
882 "type": "integer",
883 "default": 0
884 }
885 }
886 }
887 }
888 }
889 }`)
890
891 var structuralSchemaWithValidMetadataValidators = []byte(`
892 {
893 "openAPIV3Schema": {
894 "description": "CRD with CEL validators",
895 "type": "object",
896 "x-kubernetes-validations": [
897 {
898 "rule": "self.metadata.name.size() > 3"
899 }
900 ],
901 "properties": {
902 "metadata": {
903 "type": "object",
904 "properties": {
905 "name": { "type": "string" }
906 }
907 },
908 "spec": {
909 "type": "object",
910 "properties": {}
911 },
912 "status": {
913 "type": "object",
914 "properties": {}
915 }
916 }
917 }
918 }`)
919
920 var structuralSchemaWithInvalidMetadataValidators = []byte(`
921 {
922 "openAPIV3Schema": {
923 "description": "CRD with CEL validators",
924 "type": "object",
925 "x-kubernetes-validations": [
926 {
927 "rule": "self.metadata.labels.size() > 0"
928 }
929 ],
930 "properties": {
931 "metadata": {
932 "type": "object",
933 "properties": {
934 "name": { "type": "string" }
935 }
936 },
937 "spec": {
938 "type": "object",
939 "properties": {}
940 },
941 "status": {
942 "type": "object",
943 "properties": {}
944 }
945 }
946 }
947 }`)
948
949 var structuralSchemaWithValidTransitionRule = []byte(`
950 {
951 "openAPIV3Schema": {
952 "description": "CRD with CEL validators",
953 "type": "object",
954 "properties": {
955 "spec": {
956 "type": "object",
957 "properties": {
958 "someImmutableThing": { "type": "string" },
959 "somethingElse": { "type": "string" }
960 },
961 "x-kubernetes-validations": [
962 {
963 "rule": "self.someImmutableThing == oldSelf.someImmutableThing"
964 }
965 ]
966 },
967 "status": {
968 "type": "object",
969 "properties": {}
970 }
971 }
972 }
973 }`)
974
975 var structuralSchemaWithInvalidTransitionRule = []byte(`
976 {
977 "openAPIV3Schema": {
978 "description": "CRD with CEL validators",
979 "type": "object",
980 "properties": {
981 "spec": {
982 "type": "object",
983 "properties": {
984 "list": {
985 "type": "array",
986 "items": {
987 "type": "string",
988 "x-kubernetes-validations": [
989 {
990 "rule": "self == oldSelf"
991 }
992 ]
993 }
994 }
995 }
996 },
997 "status": {
998 "type": "object",
999 "properties": {}
1000 }
1001 }
1002 }
1003 }`)
1004
1005 var structuralSchemaWithDefaultMapKeyTransitionRule = []byte(`
1006 {
1007 "openAPIV3Schema": {
1008 "description": "CRD with CEL validators",
1009 "type": "object",
1010 "properties": {
1011 "spec": {
1012 "type": "object",
1013 "properties": {
1014 "list": {
1015 "type": "array",
1016 "x-kubernetes-list-map-keys": [
1017 "k1",
1018 "k2"
1019 ],
1020 "x-kubernetes-list-type": "map",
1021 "maxItems": 1000,
1022 "items": {
1023 "type": "object",
1024 "properties": {
1025 "k1": { "type": "string" },
1026 "k2": { "type": "string", "default": "DEFAULT" },
1027 "v": { "type": "string", "maxLength": 200 }
1028 },
1029 "required": ["k1"],
1030 "x-kubernetes-validations": [
1031 {
1032 "rule": "self.v == oldSelf.v"
1033 }
1034 ]
1035 }
1036 }
1037 }
1038 },
1039 "status": {
1040 "type": "object",
1041 "properties": {}
1042 }
1043 }
1044 }
1045 }`)
1046
1047 var structuralSchemaWithCostLimit = []byte(`
1048 {
1049 "openAPIV3Schema": {
1050 "description": "CRD with CEL validators",
1051 "type": "object",
1052 "properties": {
1053 "spec": {
1054 "type": "object",
1055 "properties": {
1056 "list": {
1057 "type": "array",
1058 "maxItems": 725,
1059 "items": {
1060 "type": "integer"
1061 },
1062 "x-kubernetes-validations": [
1063 {
1064 "rule": "self.all(x, self.all(y, x == y))"
1065 }
1066 ]
1067 }
1068 }
1069 },
1070 "status": {
1071 "type": "object",
1072 "properties": {}
1073 }
1074 }
1075 }
1076 }`)
1077
1078 var structuralSchemaWithEmptyObject = []byte(`
1079 {
1080 "openAPIV3Schema": {
1081 "description": "weird CRD with empty spec, unstructured status. designed to fit test fixtures.",
1082 "type": "object",
1083 "x-kubernetes-validations": [
1084 {
1085 "rule": "[has(self.spec), has(self.status)].exists_one(x, x)"
1086 }
1087 ],
1088 "properties": {
1089 "spec": {
1090 "type": "object"
1091 },
1092 "status": {
1093 "type": "object",
1094 "additionalProperties": true
1095 }
1096 }
1097 }
1098 }
1099 `)
1100
View as plain text