k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/apimachinery/crd_validation_rules.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package apimachinery 18 19 import ( 20 "context" 21 "strings" 22 23 "github.com/onsi/ginkgo/v2" 24 "github.com/onsi/gomega" 25 26 "k8s.io/apimachinery/pkg/runtime/schema" 27 28 v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 29 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 30 "k8s.io/apiextensions-apiserver/test/integration/fixtures" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 "k8s.io/apimachinery/pkg/util/json" 34 "k8s.io/apiserver/pkg/storage/names" 35 "k8s.io/client-go/dynamic" 36 "k8s.io/kubernetes/test/e2e/framework" 37 admissionapi "k8s.io/pod-security-admission/api" 38 ) 39 40 var _ = SIGDescribe("CustomResourceValidationRules [Privileged:ClusterAdmin]", func() { 41 f := framework.NewDefaultFramework("crd-validation-expressions") 42 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged 43 44 var apiExtensionClient *clientset.Clientset 45 ginkgo.BeforeEach(func() { 46 var err error 47 apiExtensionClient, err = clientset.NewForConfig(f.ClientConfig()) 48 framework.ExpectNoError(err, "initializing apiExtensionClient") 49 }) 50 51 customResourceClient := func(crd *v1.CustomResourceDefinition) (dynamic.NamespaceableResourceInterface, schema.GroupVersionResource) { 52 gvrs := fixtures.GetGroupVersionResourcesOfCustomResource(crd) 53 if len(gvrs) != 1 { 54 ginkgo.Fail("Expected one version in custom resource definition") 55 } 56 gvr := gvrs[0] 57 return f.DynamicClient.Resource(gvr), gvr 58 } 59 unmarshallSchema := func(schemaJson []byte) *v1.JSONSchemaProps { 60 var c v1.JSONSchemaProps 61 err := json.Unmarshal(schemaJson, &c) 62 framework.ExpectNoError(err, "unmarshalling OpenAPIv3 schema") 63 return &c 64 } 65 66 // for all new CRD validation features that should be E2E-tested, add them 67 // into this schema and then add CR requests to the end of the test 68 // below ("MUST NOT fail validation...") instead of writing a new and 69 // separate test 70 var schemaWithValidationExpression = unmarshallSchema([]byte(`{ 71 "type":"object", 72 "properties":{ 73 "spec":{ 74 "type":"object", 75 "x-kubernetes-validations":[ 76 { "rule":"self.x + self.y > 0" }, 77 { "rule":"self.firstArray.isSorted() && self.secondArray.isSorted() && ((self.firstArray.sum() + self.secondArray.sum()) % 2 == 0)" }, 78 { "rule":"self.largeArray.all(x, self.largeArray.all(y, y == x))" } 79 ], 80 "properties":{ 81 "x":{ "type":"integer" }, 82 "y":{ "type":"integer" }, 83 "firstArray":{ "type":"array", "maxItems": 1000, "items":{ "type": "integer"} }, 84 "secondArray":{ "type":"array", "maxItems": 1000, "items":{ "type": "integer"} }, 85 "largeArray":{ "type":"array", "maxItems": 725, "items":{ "type": "integer"} } 86 } 87 }, 88 "status":{ 89 "type":"object", 90 "x-kubernetes-validations":[ 91 { "rule":"self.health == 'ok' || self.health == 'unhealthy'" } 92 ], 93 "properties":{ 94 "health":{ "type":"string" } 95 } 96 } 97 } 98 }`)) 99 ginkgo.It("MUST NOT fail validation for create of a custom resource that satisfies the x-kubernetes-validations rules", func(ctx context.Context) { 100 ginkgo.By("Creating a custom resource definition with validation rules") 101 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithValidationExpression, false) 102 crd, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 103 framework.ExpectNoError(err, "creating CustomResourceDefinition") 104 defer func() { 105 err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient) 106 framework.ExpectNoError(err, "deleting CustomResourceDefinition") 107 }() 108 109 ginkgo.By("Creating a custom resource with values that are allowed by the validation rules set on the custom resource definition") 110 crClient, gvr := customResourceClient(crd) 111 name1 := names.SimpleNameGenerator.GenerateName("cr-1") 112 _, err = crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{ 113 "apiVersion": gvr.Group + "/" + gvr.Version, 114 "kind": crd.Spec.Names.Kind, 115 "metadata": map[string]interface{}{ 116 "name": name1, 117 "namespace": f.Namespace.Name, 118 }, 119 "spec": map[string]interface{}{ 120 "x": int64(1), 121 "y": int64(0), 122 "firstArray": []int64{3, 4}, 123 "secondArray": []int64{5, 10}, 124 "largeArray": []int64{2, 2}, 125 }, 126 }}, metav1.CreateOptions{}) 127 framework.ExpectNoError(err, "validation rules satisfied") 128 }) 129 ginkgo.It("MUST fail validation for create of a custom resource that does not satisfy the x-kubernetes-validations rules", func(ctx context.Context) { 130 ginkgo.By("Creating a custom resource definition with validation rules") 131 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithValidationExpression, false) 132 crd, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 133 framework.ExpectNoError(err, "creating CustomResourceDefinition") 134 defer func() { 135 err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient) 136 framework.ExpectNoError(err, "deleting CustomResourceDefinition") 137 }() 138 139 ginkgo.By("Creating a custom resource with values that fail the validation rules set on the custom resource definition") 140 crClient, gvr := customResourceClient(crd) 141 name1 := names.SimpleNameGenerator.GenerateName("cr-1") 142 _, err = crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{ 143 "apiVersion": gvr.Group + "/" + gvr.Version, 144 "kind": crd.Spec.Names.Kind, 145 "metadata": map[string]interface{}{ 146 "name": name1, 147 "namespace": f.Namespace.Name, 148 }, 149 "spec": map[string]interface{}{ 150 "x": int64(0), 151 "y": int64(0), 152 }, 153 }}, metav1.CreateOptions{}) 154 gomega.Expect(err).To(gomega.HaveOccurred(), "validation rules not satisfied") 155 expectedErrMsg := "failed rule" 156 if !strings.Contains(err.Error(), expectedErrMsg) { 157 framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) 158 } 159 }) 160 161 ginkgo.It("MUST fail create of a custom resource definition that contains a x-kubernetes-validations rule that refers to a property that do not exist", func(ctx context.Context) { 162 ginkgo.By("Defining a custom resource definition with a validation rule that refers to a property that do not exist") 163 var schemaWithInvalidValidationRule = unmarshallSchema([]byte(`{ 164 "type":"object", 165 "properties":{ 166 "spec":{ 167 "type":"object", 168 "x-kubernetes-validations":[ 169 { "rule":"self.z == 100" } 170 ], 171 "properties":{ 172 "x":{ "type":"integer" } 173 } 174 } 175 } 176 }`)) 177 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithInvalidValidationRule, false) 178 _, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 179 gomega.Expect(err).To(gomega.HaveOccurred(), "creating CustomResourceDefinition with a validation rule that refers to a property that do not exist") 180 expectedErrMsg := "undefined field 'z'" 181 if !strings.Contains(err.Error(), expectedErrMsg) { 182 framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) 183 } 184 }) 185 186 ginkgo.It("MUST fail create of a custom resource definition that contains an x-kubernetes-validations rule that contains a syntax error", func(ctx context.Context) { 187 ginkgo.By("Defining a custom resource definition that contains a validation rule with a syntax error") 188 var schemaWithSyntaxErrorRule = unmarshallSchema([]byte(`{ 189 "type":"object", 190 "properties":{ 191 "spec":{ 192 "type":"object", 193 "x-kubernetes-validations":[ 194 { "rule":"self = 42" } 195 ] 196 } 197 } 198 }`)) 199 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithSyntaxErrorRule, false) 200 _, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 201 gomega.Expect(err).To(gomega.HaveOccurred(), "creating a CustomResourceDefinition with a validation rule that contains a syntax error") 202 expectedErrMsg := "Syntax error" 203 if !strings.Contains(err.Error(), expectedErrMsg) { 204 framework.Failf("expected error message to contain %q, got %q", expectedErrMsg, err.Error()) 205 } 206 }) 207 208 ginkgo.It("MUST fail create of a custom resource definition that contains an x-kubernetes-validations rule that exceeds the estimated cost limit", func(ctx context.Context) { 209 ginkgo.By("Defining a custom resource definition that contains a validation rule that exceeds the cost limit") 210 var schemaWithExpensiveRule = unmarshallSchema([]byte(`{ 211 "type":"object", 212 "properties":{ 213 "spec":{ 214 "type":"object", 215 "properties":{ 216 "x":{ 217 "type":"array", 218 "items":{ 219 "type":"array", 220 "items":{ 221 "type":"string" 222 }, 223 "x-kubernetes-validations":[ 224 { "rule":"self.all(s, s == 'string constant')" } 225 ] 226 } 227 } 228 } 229 } 230 } 231 }`)) 232 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithExpensiveRule, false) 233 _, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 234 gomega.Expect(err).To(gomega.HaveOccurred(), "creating a CustomResourceDefinition with a validation rule that exceeds the cost limit") 235 expectedErrMsg := "exceeds budget" 236 if !strings.Contains(err.Error(), expectedErrMsg) { 237 framework.Failf("expected error message to contain %q, got %q", expectedErrMsg, err.Error()) 238 } 239 }) 240 241 ginkgo.It("MUST fail create of a custom resource that exceeds the runtime cost limit for x-kubernetes-validations rule execution", func(ctx context.Context) { 242 ginkgo.By("Defining a custom resource definition including an expensive rule on a large amount of data") 243 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithValidationExpression, false) 244 _, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 245 framework.ExpectNoError(err, "creating CustomResourceDefinition including an expensive rule on a large amount of data") 246 defer func() { 247 err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient) 248 framework.ExpectNoError(err, "deleting CustomResourceDefinition") 249 }() 250 ginkgo.By("Attempting to create a custom resource that will exceed the runtime cost limit") 251 crClient, gvr := customResourceClient(crd) 252 name1 := names.SimpleNameGenerator.GenerateName("cr-1") 253 _, err = crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{ 254 "apiVersion": gvr.Group + "/" + gvr.Version, 255 "kind": crd.Spec.Names.Kind, 256 "metadata": map[string]interface{}{ 257 "name": name1, 258 "namespace": f.Namespace.Name, 259 }, 260 "spec": map[string]interface{}{ 261 "largeArray": genLargeArray(725, 20), 262 }, 263 }}, metav1.CreateOptions{}) 264 gomega.Expect(err).To(gomega.HaveOccurred(), "custom resource creation should be prohibited by runtime cost limit") 265 expectedErrMsg := "call cost exceeds limit" 266 if !strings.Contains(err.Error(), expectedErrMsg) { 267 framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) 268 } 269 }) 270 271 ginkgo.It("MUST fail update of a custom resource that does not satisfy a x-kubernetes-validations transition rule", func(ctx context.Context) { 272 ginkgo.By("Defining a custom resource definition with a x-kubernetes-validations transition rule") 273 var schemaWithTransitionRule = unmarshallSchema([]byte(`{ 274 "type":"object", 275 "properties":{ 276 "spec":{ 277 "type":"object", 278 "properties":{ 279 "num":{ 280 "type":"integer", 281 "x-kubernetes-validations":[ 282 { "rule":"self > oldSelf" } 283 ] 284 } 285 } 286 } 287 } 288 }`)) 289 crd := fixtures.NewRandomNameV1CustomResourceDefinitionWithSchema(v1.NamespaceScoped, schemaWithTransitionRule, false) 290 _, err := fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient) 291 framework.ExpectNoError(err, "creating CustomResourceDefinition including an x-kubernetes-validations transition rule") 292 defer func() { 293 err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient) 294 framework.ExpectNoError(err, "deleting CustomResourceDefinition") 295 }() 296 ginkgo.By("Attempting to create a custom resource") 297 crClient, gvr := customResourceClient(crd) 298 name1 := names.SimpleNameGenerator.GenerateName("cr-1") 299 unstruct, err := crClient.Namespace(f.Namespace.Name).Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{ 300 "apiVersion": gvr.Group + "/" + gvr.Version, 301 "kind": crd.Spec.Names.Kind, 302 "metadata": map[string]interface{}{ 303 "name": name1, 304 "namespace": f.Namespace.Name, 305 }, 306 "spec": map[string]interface{}{ 307 "num": int64(10), 308 }, 309 }}, metav1.CreateOptions{}) 310 framework.ExpectNoError(err, "transition rules do not apply to create operations") 311 ginkgo.By("Updating a custom resource with a value that does not satisfy an x-kubernetes-validations transition rule") 312 _, err = crClient.Namespace(f.Namespace.Name).Update(ctx, &unstructured.Unstructured{Object: map[string]interface{}{ 313 "apiVersion": gvr.Group + "/" + gvr.Version, 314 "kind": crd.Spec.Names.Kind, 315 "metadata": map[string]interface{}{ 316 "name": name1, 317 "namespace": f.Namespace.Name, 318 "resourceVersion": unstruct.GetResourceVersion(), 319 }, 320 "spec": map[string]interface{}{ 321 "num": int64(9), 322 }, 323 }}, metav1.UpdateOptions{}) 324 gomega.Expect(err).To(gomega.HaveOccurred(), "custom resource update should be prohibited by transition rule") 325 expectedErrMsg := "failed rule" 326 if !strings.Contains(err.Error(), expectedErrMsg) { 327 framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error()) 328 } 329 }) 330 }) 331 332 func genLargeArray(n, x int64) []int64 { 333 arr := make([]int64, n) 334 for i := int64(0); i < n; i++ { 335 arr[i] = x 336 } 337 return arr 338 }