k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/apimachinery/validatingadmissionpolicy.go (about) 1 /* 2 Copyright 2022 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 "fmt" 22 "math/rand/v2" 23 "time" 24 25 "github.com/onsi/ginkgo/v2" 26 "github.com/onsi/gomega" 27 28 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 29 appsv1 "k8s.io/api/apps/v1" 30 v1 "k8s.io/api/core/v1" 31 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 32 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 33 apierrors "k8s.io/apimachinery/pkg/api/errors" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 "k8s.io/apimachinery/pkg/types" 37 utilrand "k8s.io/apimachinery/pkg/util/rand" 38 "k8s.io/apimachinery/pkg/util/wait" 39 "k8s.io/apimachinery/pkg/watch" 40 applyadmissionregistrationv1 "k8s.io/client-go/applyconfigurations/admissionregistration/v1" 41 clientset "k8s.io/client-go/kubernetes" 42 "k8s.io/client-go/openapi3" 43 "k8s.io/client-go/util/retry" 44 "k8s.io/kubernetes/test/e2e/framework" 45 admissionapi "k8s.io/pod-security-admission/api" 46 ) 47 48 var _ = SIGDescribe("ValidatingAdmissionPolicy [Privileged:ClusterAdmin]", func() { 49 f := framework.NewDefaultFramework("validating-admission-policy") 50 f.NamespacePodSecurityLevel = admissionapi.LevelBaseline 51 52 var client clientset.Interface 53 var extensionsClient apiextensionsclientset.Interface 54 55 ginkgo.BeforeEach(func() { 56 var err error 57 client, err = clientset.NewForConfig(f.ClientConfig()) 58 framework.ExpectNoError(err, "initializing client") 59 extensionsClient, err = apiextensionsclientset.NewForConfig(f.ClientConfig()) 60 framework.ExpectNoError(err, "initializing api-extensions client") 61 }) 62 63 ginkgo.BeforeEach(func(ctx context.Context) { 64 // Make sure the namespace created for the test is labeled to be selected 65 // in binding.spec.matchResources.namespaceSelector.matchLabels 66 // By containing the tests within the marked namespace, they will not 67 // disturb concurrent tests that run in other namespaces. 68 labelNamespace(ctx, f, f.Namespace.Name) 69 }) 70 71 /* 72 Release: v1.30 73 Testname: ValidatingAdmissionPolicy 74 Description: 75 The ValidatingAdmissionPolicy should validate a deployment as the expression defined inside the policy. 76 */ 77 framework.ConformanceIt("should validate against a Deployment", func(ctx context.Context) { 78 ginkgo.By("creating the policy", func() { 79 policy := newValidatingAdmissionPolicyBuilder(f.UniqueName+".policy.example.com"). 80 MatchUniqueNamespace(f.UniqueName). 81 StartResourceRule(). 82 MatchResource([]string{"apps"}, []string{"v1"}, []string{"deployments"}). 83 EndResourceRule(). 84 WithValidation(admissionregistrationv1.Validation{ 85 Expression: "object.spec.replicas > 1", 86 MessageExpression: "'wants replicas > 1, got ' + object.spec.replicas", 87 }). 88 WithValidation(admissionregistrationv1.Validation{ 89 Expression: "namespaceObject.metadata.name == '" + f.UniqueName + "'", 90 Message: "Internal error! Other namespace should not be allowed.", 91 }). 92 Build() 93 policy, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}) 94 framework.ExpectNoError(err, "create policy") 95 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 96 return client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{}) 97 }, policy.Name) 98 binding := createBinding(f.UniqueName+".binding.example.com", f.UniqueName, policy.Name) 99 binding, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicyBindings().Create(ctx, binding, metav1.CreateOptions{}) 100 framework.ExpectNoError(err, "create policy binding") 101 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 102 return client.AdmissionregistrationV1().ValidatingAdmissionPolicyBindings().Delete(ctx, name, metav1.DeleteOptions{}) 103 }, binding.Name) 104 }) 105 ginkgo.By("waiting until the marker is denied", func() { 106 deployment := basicDeployment("marker-deployment", 1) 107 err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 108 _, err = client.AppsV1().Deployments(f.Namespace.Name).Create(ctx, deployment, metav1.CreateOptions{}) 109 defer client.AppsV1().Deployments(f.Namespace.Name).Delete(ctx, deployment.Name, metav1.DeleteOptions{}) 110 if err != nil { 111 if apierrors.IsInvalid(err) { 112 return true, nil 113 } 114 return false, err 115 } 116 return false, nil 117 }) 118 framework.ExpectNoError(err, "wait for marker") 119 }) 120 ginkgo.By("testing a replicated Deployment to be allowed", func() { 121 deployment := basicDeployment("replicated", 2) 122 deployment, err := client.AppsV1().Deployments(f.Namespace.Name).Create(ctx, deployment, metav1.CreateOptions{}) 123 defer client.AppsV1().Deployments(f.Namespace.Name).Delete(ctx, deployment.Name, metav1.DeleteOptions{}) 124 framework.ExpectNoError(err, "create replicated Deployment") 125 }) 126 ginkgo.By("testing a non-replicated ReplicaSet not to be denied", func() { 127 replicaSet := basicReplicaSet("non-replicated", 1) 128 replicaSet, err := client.AppsV1().ReplicaSets(f.Namespace.Name).Create(ctx, replicaSet, metav1.CreateOptions{}) 129 defer client.AppsV1().ReplicaSets(f.Namespace.Name).Delete(ctx, replicaSet.Name, metav1.DeleteOptions{}) 130 framework.ExpectNoError(err, "create non-replicated ReplicaSet") 131 }) 132 }) 133 134 /* 135 Release: v1.30 136 Testname: ValidatingAdmissionPolicy 137 Description: 138 The ValidatingAdmissionPolicy should type check the expressions defined inside policy. 139 */ 140 framework.It("should type check validation expressions", func(ctx context.Context) { 141 var policy *admissionregistrationv1.ValidatingAdmissionPolicy 142 ginkgo.By("creating the policy with correct types", func() { 143 policy = newValidatingAdmissionPolicyBuilder(f.UniqueName+".correct-policy.example.com"). 144 MatchUniqueNamespace(f.UniqueName). 145 StartResourceRule(). 146 MatchResource([]string{"apps"}, []string{"v1"}, []string{"deployments"}). 147 EndResourceRule(). 148 WithValidation(admissionregistrationv1.Validation{ 149 Expression: "object.spec.replicas > 1", 150 }). 151 Build() 152 var err error 153 policy, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}) 154 framework.ExpectNoError(err, "create policy") 155 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 156 return client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{}) 157 }, policy.Name) 158 }) 159 ginkgo.By("waiting for the type check to finish without any warnings", func() { 160 err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 161 policy, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{}) 162 if err != nil { 163 return false, err 164 } 165 if policy.Status.TypeChecking != nil { // non-nil TypeChecking indicates its completion 166 return true, nil 167 } 168 return false, nil 169 }) 170 framework.ExpectNoError(err, "wait for type checking") 171 gomega.Expect(policy.Status.TypeChecking.ExpressionWarnings).To(gomega.BeEmpty()) 172 }) 173 ginkgo.By("creating the policy with type confusion", func() { 174 policy = newValidatingAdmissionPolicyBuilder(f.UniqueName+".confused-policy.example.com"). 175 MatchUniqueNamespace(f.UniqueName). 176 StartResourceRule(). 177 MatchResource([]string{"apps"}, []string{"v1"}, []string{"deployments"}). 178 EndResourceRule(). 179 WithValidation(admissionregistrationv1.Validation{ 180 Expression: "object.spec.replicas > '1'", // confusion: int > string 181 MessageExpression: "'wants replicas > 1, got ' + object.spec.replicas", // confusion: string + int 182 }). 183 Build() 184 var err error 185 policy, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}) 186 framework.ExpectNoError(err, "create policy") 187 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 188 return client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{}) 189 }, policy.Name) 190 }) 191 ginkgo.By("waiting for the type check to finish with warnings", func() { 192 err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 193 policy, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{}) 194 if err != nil { 195 return false, err 196 } 197 if policy.Status.TypeChecking != nil { // non-nil TypeChecking indicates its completion 198 return true, nil 199 } 200 return false, nil 201 }) 202 framework.ExpectNoError(err, "wait for type checking") 203 204 // assert it to contain 2 warnings, first for expression and second for messageExpression 205 gomega.Expect(policy.Status.TypeChecking.ExpressionWarnings).To(gomega.HaveLen(2)) 206 warning := policy.Status.TypeChecking.ExpressionWarnings[0] 207 gomega.Expect(warning.FieldRef).To(gomega.Equal("spec.validations[0].expression")) 208 gomega.Expect(warning.Warning).To(gomega.ContainSubstring("found no matching overload for '_>_' applied to '(int, string)'")) 209 warning = policy.Status.TypeChecking.ExpressionWarnings[1] 210 gomega.Expect(warning.FieldRef).To(gomega.Equal("spec.validations[0].messageExpression")) 211 gomega.Expect(warning.Warning).To(gomega.ContainSubstring("found no matching overload for '_+_' applied to '(string, int)'")) 212 }) 213 }) 214 215 /* 216 Release: v1.30 217 Testname: ValidatingAdmissionPolicy 218 Description: 219 The ValidatingAdmissionPolicy should allow expressions to refer variables. 220 */ 221 framework.ConformanceIt("should allow expressions to refer variables.", func(ctx context.Context) { 222 ginkgo.By("creating a policy with variables", func() { 223 policy := newValidatingAdmissionPolicyBuilder(f.UniqueName+".policy.example.com"). 224 MatchUniqueNamespace(f.UniqueName). 225 StartResourceRule(). 226 MatchResource([]string{"apps"}, []string{"v1"}, []string{"deployments"}). 227 EndResourceRule(). 228 WithVariable(admissionregistrationv1.Variable{ 229 Name: "replicas", 230 Expression: "object.spec.replicas", 231 }). 232 WithVariable(admissionregistrationv1.Variable{ 233 Name: "oddReplicas", 234 Expression: "variables.replicas % 2 == 1", 235 }). 236 WithValidation(admissionregistrationv1.Validation{ 237 Expression: "variables.replicas > 1", 238 }). 239 WithValidation(admissionregistrationv1.Validation{ 240 Expression: "variables.oddReplicas", 241 }). 242 Build() 243 policy, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}) 244 framework.ExpectNoError(err, "create policy") 245 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 246 return client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{}) 247 }, policy.Name) 248 binding := createBinding(f.UniqueName+".binding.example.com", f.UniqueName, policy.Name) 249 binding, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicyBindings().Create(ctx, binding, metav1.CreateOptions{}) 250 framework.ExpectNoError(err, "create policy binding") 251 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 252 return client.AdmissionregistrationV1().ValidatingAdmissionPolicyBindings().Delete(ctx, name, metav1.DeleteOptions{}) 253 }, binding.Name) 254 }) 255 ginkgo.By("waiting until the marker is denied", func() { 256 deployment := basicDeployment("marker-deployment", 1) 257 err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 258 _, err = client.AppsV1().Deployments(f.Namespace.Name).Create(ctx, deployment, metav1.CreateOptions{}) 259 defer client.AppsV1().Deployments(f.Namespace.Name).Delete(ctx, deployment.Name, metav1.DeleteOptions{}) 260 if err != nil { 261 if apierrors.IsInvalid(err) { 262 return true, nil 263 } 264 return false, err 265 } 266 return false, nil 267 }) 268 framework.ExpectNoError(err, "wait for marker") 269 }) 270 ginkgo.By("testing a replicated Deployment to be allowed", func() { 271 deployment := basicDeployment("replicated", 3) 272 deployment, err := client.AppsV1().Deployments(f.Namespace.Name).Create(ctx, deployment, metav1.CreateOptions{}) 273 defer client.AppsV1().Deployments(f.Namespace.Name).Delete(ctx, deployment.Name, metav1.DeleteOptions{}) 274 framework.ExpectNoError(err, "create replicated Deployment") 275 }) 276 ginkgo.By("testing a non-replicated ReplicaSet not to be denied", func() { 277 replicaSet := basicReplicaSet("non-replicated", 1) 278 replicaSet, err := client.AppsV1().ReplicaSets(f.Namespace.Name).Create(ctx, replicaSet, metav1.CreateOptions{}) 279 defer client.AppsV1().ReplicaSets(f.Namespace.Name).Delete(ctx, replicaSet.Name, metav1.DeleteOptions{}) 280 framework.ExpectNoError(err, "create non-replicated ReplicaSet") 281 }) 282 }) 283 284 /* 285 Release: v1.30 286 Testname: ValidatingAdmissionPolicy 287 Description: 288 The ValidatingAdmissionPolicy should type check a CRD. 289 */ 290 framework.It("should type check a CRD", func(ctx context.Context) { 291 crd := crontabExampleCRD() 292 crd.Spec.Group = "stable." + f.UniqueName 293 crd.Name = crd.Spec.Names.Plural + "." + crd.Spec.Group 294 var policy *admissionregistrationv1.ValidatingAdmissionPolicy 295 ginkgo.By("creating the CRD", func() { 296 var err error 297 crd, err = extensionsClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}) 298 framework.ExpectNoError(err, "create CRD") 299 err = wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 300 // wait for the CRD to be published. 301 root := openapi3.NewRoot(client.Discovery().OpenAPIV3()) 302 _, err = root.GVSpec(schema.GroupVersion{Group: crd.Spec.Group, Version: "v1"}) 303 return err == nil, nil 304 }) 305 framework.ExpectNoError(err, "wait for CRD.") 306 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 307 return extensionsClient.ApiextensionsV1().CustomResourceDefinitions().Delete(ctx, name, metav1.DeleteOptions{}) 308 }, crd.Name) 309 }) 310 ginkgo.By("creating a vaild policy for crontabs", func() { 311 policy = newValidatingAdmissionPolicyBuilder(f.UniqueName+".correct-crd-policy.example.com"). 312 MatchUniqueNamespace(f.UniqueName). 313 StartResourceRule(). 314 MatchResource([]string{crd.Spec.Group}, []string{"v1"}, []string{"crontabs"}). 315 EndResourceRule(). 316 WithValidation(admissionregistrationv1.Validation{ 317 Expression: "object.spec.replicas > 1", 318 }). 319 Build() 320 policy, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}) 321 framework.ExpectNoError(err, "create policy") 322 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 323 return client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{}) 324 }, policy.Name) 325 }) 326 ginkgo.By("waiting for the type check to finish without warnings", func() { 327 err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 328 policy, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{}) 329 if err != nil { 330 return false, err 331 } 332 if policy.Status.TypeChecking != nil { 333 return true, nil 334 } 335 return false, nil 336 }) 337 framework.ExpectNoError(err, "wait for type checking") 338 gomega.Expect(policy.Status.TypeChecking.ExpressionWarnings).To(gomega.BeEmpty(), "expect no warnings") 339 }) 340 ginkgo.By("creating a policy with type-confused expressions for crontabs", func() { 341 policy = newValidatingAdmissionPolicyBuilder(f.UniqueName+".confused-crd-policy.example.com"). 342 MatchUniqueNamespace(f.UniqueName). 343 StartResourceRule(). 344 MatchResource([]string{crd.Spec.Group}, []string{"v1"}, []string{"crontabs"}). 345 EndResourceRule(). 346 WithValidation(admissionregistrationv1.Validation{ 347 Expression: "object.spec.replicas > '1'", // type confusion 348 }). 349 WithValidation(admissionregistrationv1.Validation{ 350 Expression: "object.spec.maxRetries < 10", // not yet existing field 351 }). 352 Build() 353 policy, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(ctx, policy, metav1.CreateOptions{}) 354 framework.ExpectNoError(err, "create policy") 355 ginkgo.DeferCleanup(func(ctx context.Context, name string) error { 356 return client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Delete(ctx, name, metav1.DeleteOptions{}) 357 }, policy.Name) 358 }) 359 ginkgo.By("waiting for the type check to finish with warnings", func() { 360 err := wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) { 361 policy, err = client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, policy.Name, metav1.GetOptions{}) 362 if err != nil { 363 return false, err 364 } 365 if policy.Status.TypeChecking != nil { 366 // TODO(#123829) Remove once the schema watcher is merged. 367 // If the warnings are empty, touch the policy to retry type checking 368 if len(policy.Status.TypeChecking.ExpressionWarnings) == 0 { 369 applyConfig := applyadmissionregistrationv1.ValidatingAdmissionPolicy(policy.Name).WithLabels(map[string]string{ 370 "touched": time.Now().String(), 371 "random": fmt.Sprintf("%d", rand.Int()), 372 }) 373 _, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Apply(ctx, applyConfig, metav1.ApplyOptions{}) 374 return false, err 375 } 376 return true, nil 377 } 378 return false, nil 379 }) 380 framework.ExpectNoError(err, "wait for type checking") 381 382 gomega.Expect(policy.Status.TypeChecking.ExpressionWarnings).To(gomega.HaveLen(2)) 383 warning := policy.Status.TypeChecking.ExpressionWarnings[0] 384 gomega.Expect(warning.FieldRef).To(gomega.Equal("spec.validations[0].expression")) 385 gomega.Expect(warning.Warning).To(gomega.ContainSubstring("found no matching overload for '_>_' applied to '(int, string)'")) 386 warning = policy.Status.TypeChecking.ExpressionWarnings[1] 387 gomega.Expect(warning.FieldRef).To(gomega.Equal("spec.validations[1].expression")) 388 gomega.Expect(warning.Warning).To(gomega.ContainSubstring("undefined field 'maxRetries'")) 389 }) 390 }) 391 392 /* 393 Release: v1.30 394 Testname: ValidatingAdmissionPolicy API 395 Description: 396 The admissionregistration.k8s.io API group MUST exist in the 397 /apis discovery document. 398 The admissionregistration.k8s.io/v1 API group/version MUST exist 399 in the /apis/admissionregistration.k8s.io discovery document. 400 The validatingadmisionpolicy and validatingadmissionpolicy/status 401 resources MUST exist in the 402 /apis/admissionregistration.k8s.io/v1 discovery document. 403 The validatingadmisionpolicy resource must support create, get, 404 list, watch, update, patch, delete, and deletecollection. 405 */ 406 framework.ConformanceIt("should support ValidatingAdmissionPolicy API operations", func(ctx context.Context) { 407 vapVersion := "v1" 408 ginkgo.By("getting /apis") 409 { 410 discoveryGroups, err := f.ClientSet.Discovery().ServerGroups() 411 framework.ExpectNoError(err) 412 found := false 413 for _, group := range discoveryGroups.Groups { 414 if group.Name == admissionregistrationv1.GroupName { 415 for _, version := range group.Versions { 416 if version.Version == vapVersion { 417 found = true 418 break 419 } 420 } 421 } 422 } 423 if !found { 424 framework.Failf("expected ValidatingAdmissionPolicy API group/version, got %#v", discoveryGroups.Groups) 425 } 426 } 427 428 ginkgo.By("getting /apis/admissionregistration.k8s.io") 429 { 430 group := &metav1.APIGroup{} 431 err := f.ClientSet.Discovery().RESTClient().Get().AbsPath("/apis/admissionregistration.k8s.io").Do(ctx).Into(group) 432 framework.ExpectNoError(err) 433 found := false 434 for _, version := range group.Versions { 435 if version.Version == vapVersion { 436 found = true 437 break 438 } 439 } 440 if !found { 441 framework.Failf("expected ValidatingAdmissionPolicy API version, got %#v", group.Versions) 442 } 443 } 444 445 ginkgo.By("getting /apis/admissionregistration.k8s.io/" + vapVersion) 446 { 447 resources, err := f.ClientSet.Discovery().ServerResourcesForGroupVersion(admissionregistrationv1.SchemeGroupVersion.String()) 448 framework.ExpectNoError(err) 449 foundVAP, foundVAPStatus := false, false 450 for _, resource := range resources.APIResources { 451 switch resource.Name { 452 case "validatingadmissionpolicies": 453 foundVAP = true 454 case "validatingadmissionpolicies/status": 455 foundVAPStatus = true 456 } 457 } 458 if !foundVAP { 459 framework.Failf("expected validatingadmissionpolicies, got %#v", resources.APIResources) 460 } 461 if !foundVAPStatus { 462 framework.Failf("expected validatingadmissionpolicies/status, got %#v", resources.APIResources) 463 } 464 } 465 466 client := f.ClientSet.AdmissionregistrationV1().ValidatingAdmissionPolicies() 467 labelKey, labelValue := "example-e2e-vap-label", utilrand.String(8) 468 label := fmt.Sprintf("%s=%s", labelKey, labelValue) 469 470 template := &admissionregistrationv1.ValidatingAdmissionPolicy{ 471 ObjectMeta: metav1.ObjectMeta{ 472 GenerateName: "e2e-example-vap-", 473 Labels: map[string]string{ 474 labelKey: labelValue, 475 }, 476 }, 477 Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{ 478 Validations: []admissionregistrationv1.Validation{ 479 { 480 Expression: "object.spec.replicas <= 100", 481 }, 482 }, 483 MatchConstraints: &admissionregistrationv1.MatchResources{ 484 ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ 485 { 486 RuleWithOperations: admissionregistrationv1.RuleWithOperations{ 487 Operations: []admissionregistrationv1.OperationType{"CREATE"}, 488 Rule: admissionregistrationv1.Rule{ 489 APIGroups: []string{"apps"}, 490 APIVersions: []string{"v1"}, 491 Resources: []string{"deployments"}, 492 }, 493 }, 494 }, 495 }, 496 }, 497 }, 498 } 499 500 ginkgo.DeferCleanup(func(ctx context.Context) { 501 err := client.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: label}) 502 framework.ExpectNoError(err) 503 }) 504 505 ginkgo.By("creating") 506 _, err := client.Create(ctx, template, metav1.CreateOptions{}) 507 framework.ExpectNoError(err) 508 _, err = client.Create(ctx, template, metav1.CreateOptions{}) 509 framework.ExpectNoError(err) 510 vapCreated, err := client.Create(ctx, template, metav1.CreateOptions{}) 511 framework.ExpectNoError(err) 512 513 ginkgo.By("getting") 514 vapRead, err := client.Get(ctx, vapCreated.Name, metav1.GetOptions{}) 515 framework.ExpectNoError(err) 516 gomega.Expect(vapRead.UID).To(gomega.Equal(vapCreated.UID)) 517 518 ginkgo.By("listing") 519 list, err := client.List(ctx, metav1.ListOptions{LabelSelector: label}) 520 framework.ExpectNoError(err) 521 522 ginkgo.By("watching") 523 framework.Logf("starting watch") 524 vapWatch, err := client.Watch(ctx, metav1.ListOptions{ResourceVersion: list.ResourceVersion, LabelSelector: label}) 525 framework.ExpectNoError(err) 526 527 ginkgo.By("patching") 528 patchBytes := []byte(`{"metadata":{"annotations":{"patched":"true"}},"spec":{"failurePolicy":"Ignore"}}`) 529 vapPatched, err := client.Patch(ctx, vapCreated.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}) 530 framework.ExpectNoError(err) 531 gomega.Expect(vapPatched.Annotations).To(gomega.HaveKeyWithValue("patched", "true"), "patched object should have the applied annotation") 532 gomega.Expect(vapPatched.Spec.FailurePolicy).To(gomega.HaveValue(gomega.Equal(admissionregistrationv1.Ignore)), "patched object should have the applied spec") 533 534 ginkgo.By("updating") 535 var vapUpdated *admissionregistrationv1.ValidatingAdmissionPolicy 536 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 537 vap, err := client.Get(ctx, vapCreated.Name, metav1.GetOptions{}) 538 framework.ExpectNoError(err) 539 540 vapToUpdate := vap.DeepCopy() 541 vapToUpdate.Annotations["updated"] = "true" 542 fail := admissionregistrationv1.Fail 543 vapToUpdate.Spec.FailurePolicy = &fail 544 545 vapUpdated, err = client.Update(ctx, vapToUpdate, metav1.UpdateOptions{}) 546 return err 547 }) 548 framework.ExpectNoError(err, "failed to update validatingadmissionpolicy %q", vapCreated.Name) 549 gomega.Expect(vapUpdated.Annotations).To(gomega.HaveKeyWithValue("updated", "true"), "updated object should have the applied annotation") 550 gomega.Expect(vapUpdated.Spec.FailurePolicy).To(gomega.HaveValue(gomega.Equal(admissionregistrationv1.Fail)), "updated object should have the applied spec") 551 552 framework.Logf("waiting for watch events with expected annotations") 553 for sawAnnotation := false; !sawAnnotation; { 554 select { 555 case evt, ok := <-vapWatch.ResultChan(): 556 if !ok { 557 framework.Fail("watch channel should not close") 558 } 559 gomega.Expect(evt.Type).To(gomega.Equal(watch.Modified)) 560 vapWatched, isFS := evt.Object.(*admissionregistrationv1.ValidatingAdmissionPolicy) 561 if !isFS { 562 framework.Failf("expected an object of type: %T, but got %T", &admissionregistrationv1.ValidatingAdmissionPolicy{}, evt.Object) 563 } 564 if vapWatched.Annotations["patched"] == "true" { 565 sawAnnotation = true 566 vapWatch.Stop() 567 } else { 568 framework.Logf("missing expected annotations, waiting: %#v", vapWatched.Annotations) 569 } 570 case <-time.After(wait.ForeverTestTimeout): 571 framework.Fail("timed out waiting for watch event") 572 } 573 } 574 575 ginkgo.By("getting /status") 576 resource := admissionregistrationv1.SchemeGroupVersion.WithResource("validatingadmissionpolicies") 577 vapStatusRead, err := f.DynamicClient.Resource(resource).Get(ctx, vapCreated.Name, metav1.GetOptions{}, "status") 578 framework.ExpectNoError(err) 579 gomega.Expect(vapStatusRead.GetObjectKind().GroupVersionKind()).To(gomega.Equal(admissionregistrationv1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicy"))) 580 gomega.Expect(vapStatusRead.GetUID()).To(gomega.Equal(vapCreated.UID)) 581 582 ginkgo.By("patching /status") 583 patchBytes = []byte(`{"status":{"conditions":[{"type":"PatchStatusFailed","status":"False","reason":"e2e","message":"Set from an e2e test","lastTransitionTime":"2024-01-01T00:00:00Z"}]}}`) 584 vapStatusPatched, err := client.Patch(ctx, vapCreated.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, "status") 585 framework.ExpectNoError(err) 586 hasCondition := false 587 for i := range vapStatusPatched.Status.Conditions { 588 if vapStatusPatched.Status.Conditions[i].Type == "PatchStatusFailed" { 589 hasCondition = true 590 } 591 } 592 gomega.Expect(hasCondition).To(gomega.BeTrueBecause("expect the patched status exist")) 593 594 ginkgo.By("updating /status") 595 var vapStatusUpdated *admissionregistrationv1.ValidatingAdmissionPolicy 596 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 597 vap, err := client.Get(ctx, vapCreated.Name, metav1.GetOptions{}) 598 framework.ExpectNoError(err) 599 600 vapStatusToUpdate := vap.DeepCopy() 601 vapStatusToUpdate.Status.Conditions = append(vapStatusToUpdate.Status.Conditions, metav1.Condition{ 602 Type: "StatusUpdateFailed", 603 Status: metav1.ConditionFalse, 604 Reason: "E2E", 605 Message: "Set from an e2e test", 606 LastTransitionTime: metav1.NewTime(time.Now()), 607 }) 608 vapStatusUpdated, err = client.UpdateStatus(ctx, vapStatusToUpdate, metav1.UpdateOptions{}) 609 return err 610 }) 611 framework.ExpectNoError(err, "failed to update status of validatingadmissionpolicy %q", vapCreated.Name) 612 hasCondition = false 613 for i := range vapStatusUpdated.Status.Conditions { 614 if vapStatusUpdated.Status.Conditions[i].Type == "StatusUpdateFailed" { 615 hasCondition = true 616 } 617 } 618 gomega.Expect(hasCondition).To(gomega.BeTrueBecause("expect the updated status exist")) 619 620 ginkgo.By("deleting") 621 err = client.Delete(ctx, vapCreated.Name, metav1.DeleteOptions{}) 622 framework.ExpectNoError(err) 623 vapTmp, err := client.Get(ctx, vapCreated.Name, metav1.GetOptions{}) 624 switch { 625 case err == nil && vapTmp.GetDeletionTimestamp() != nil && len(vapTmp.GetFinalizers()) > 0: 626 // deletion requested successfully, object is blocked by finalizers 627 case err == nil: 628 framework.Failf("expected deleted object, got %#v", vapTmp) 629 case apierrors.IsNotFound(err): 630 // deleted successfully 631 default: 632 framework.Failf("expected 404, got %#v", err) 633 } 634 635 list, err = client.List(ctx, metav1.ListOptions{LabelSelector: label}) 636 var itemsWithoutFinalizer []admissionregistrationv1.ValidatingAdmissionPolicy 637 for _, item := range list.Items { 638 if len(item.GetFinalizers()) == 0 { 639 itemsWithoutFinalizer = append(itemsWithoutFinalizer, item) 640 } 641 } 642 framework.ExpectNoError(err) 643 gomega.Expect(itemsWithoutFinalizer).To(gomega.HaveLen(2), "filtered list should have 2 items") 644 645 ginkgo.By("deleting a collection") 646 err = client.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: label}) 647 framework.ExpectNoError(err) 648 649 list, err = client.List(ctx, metav1.ListOptions{LabelSelector: label}) 650 var itemsColWithoutFinalizer []admissionregistrationv1.ValidatingAdmissionPolicy 651 for _, item := range list.Items { 652 if !(item.GetDeletionTimestamp() != nil && len(item.GetFinalizers()) > 0) { 653 itemsColWithoutFinalizer = append(itemsColWithoutFinalizer, item) 654 } 655 } 656 framework.ExpectNoError(err) 657 gomega.Expect(itemsColWithoutFinalizer).To(gomega.BeEmpty(), "filtered list should have 0 items") 658 }) 659 660 /* 661 Release: v1.30 662 Testname: ValidatingadmissionPolicyBinding API 663 Description: 664 The admissionregistration.k8s.io API group MUST exist in the 665 /apis discovery document. 666 The admissionregistration.k8s.io/v1 API group/version MUST exist 667 in the /apis/admissionregistration.k8s.io discovery document. 668 The ValidatingadmissionPolicyBinding resources MUST exist in the 669 /apis/admissionregistration.k8s.io/v1 discovery document. 670 The ValidatingadmissionPolicyBinding resource must support create, get, 671 list, watch, update, patch, delete, and deletecollection. 672 */ 673 framework.ConformanceIt("should support ValidatingAdmissionPolicyBinding API operations", func(ctx context.Context) { 674 vapbVersion := "v1" 675 ginkgo.By("getting /apis") 676 { 677 discoveryGroups, err := f.ClientSet.Discovery().ServerGroups() 678 framework.ExpectNoError(err) 679 found := false 680 for _, group := range discoveryGroups.Groups { 681 if group.Name == admissionregistrationv1.GroupName { 682 for _, version := range group.Versions { 683 if version.Version == vapbVersion { 684 found = true 685 break 686 } 687 } 688 } 689 } 690 if !found { 691 framework.Failf("expected ValidatingAdmissionPolicyBinding API group/version, got %#v", discoveryGroups.Groups) 692 } 693 } 694 695 ginkgo.By("getting /apis/admissionregistration.k8s.io") 696 { 697 group := &metav1.APIGroup{} 698 err := f.ClientSet.Discovery().RESTClient().Get().AbsPath("/apis/admissionregistration.k8s.io").Do(ctx).Into(group) 699 framework.ExpectNoError(err) 700 found := false 701 for _, version := range group.Versions { 702 if version.Version == vapbVersion { 703 found = true 704 break 705 } 706 } 707 if !found { 708 framework.Failf("expected ValidatingAdmissionPolicyBinding API version, got %#v", group.Versions) 709 } 710 } 711 712 ginkgo.By("getting /apis/admissionregistration.k8s.io/" + vapbVersion) 713 { 714 resources, err := f.ClientSet.Discovery().ServerResourcesForGroupVersion(admissionregistrationv1.SchemeGroupVersion.String()) 715 framework.ExpectNoError(err) 716 foundVAPB := false 717 for _, resource := range resources.APIResources { 718 switch resource.Name { 719 case "validatingadmissionpolicybindings": 720 foundVAPB = true 721 } 722 } 723 if !foundVAPB { 724 framework.Failf("expected validatingadmissionpolicybindings, got %#v", resources.APIResources) 725 } 726 } 727 728 client := f.ClientSet.AdmissionregistrationV1().ValidatingAdmissionPolicyBindings() 729 labelKey, labelValue := "example-e2e-vapb-label", utilrand.String(8) 730 label := fmt.Sprintf("%s=%s", labelKey, labelValue) 731 732 template := &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ 733 ObjectMeta: metav1.ObjectMeta{ 734 GenerateName: "e2e-example-vapb-", 735 Labels: map[string]string{ 736 labelKey: labelValue, 737 }, 738 }, 739 Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ 740 PolicyName: "replicalimit-policy.example.com", 741 ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny}, 742 }, 743 } 744 745 ginkgo.DeferCleanup(func(ctx context.Context) { 746 err := client.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: label}) 747 framework.ExpectNoError(err) 748 }) 749 750 ginkgo.By("creating") 751 _, err := client.Create(ctx, template, metav1.CreateOptions{}) 752 framework.ExpectNoError(err) 753 _, err = client.Create(ctx, template, metav1.CreateOptions{}) 754 framework.ExpectNoError(err) 755 vapbCreated, err := client.Create(ctx, template, metav1.CreateOptions{}) 756 framework.ExpectNoError(err) 757 758 ginkgo.By("getting") 759 vapbRead, err := client.Get(ctx, vapbCreated.Name, metav1.GetOptions{}) 760 framework.ExpectNoError(err) 761 gomega.Expect(vapbRead.UID).To(gomega.Equal(vapbCreated.UID)) 762 763 ginkgo.By("listing") 764 list, err := client.List(ctx, metav1.ListOptions{LabelSelector: label}) 765 framework.ExpectNoError(err) 766 767 ginkgo.By("watching") 768 framework.Logf("starting watch") 769 vapbWatch, err := client.Watch(ctx, metav1.ListOptions{ResourceVersion: list.ResourceVersion, LabelSelector: label}) 770 framework.ExpectNoError(err) 771 772 ginkgo.By("patching") 773 patchBytes := []byte(`{"metadata":{"annotations":{"patched":"true"}},"spec":{"validationActions":["Warn"]}}`) 774 vapbPatched, err := client.Patch(ctx, vapbCreated.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}) 775 framework.ExpectNoError(err) 776 gomega.Expect(vapbPatched.Annotations).To(gomega.HaveKeyWithValue("patched", "true"), "patched object should have the applied annotation") 777 gomega.Expect(vapbPatched.Spec.ValidationActions).To(gomega.Equal([]admissionregistrationv1.ValidationAction{admissionregistrationv1.Warn}), "patched object should have the applied spec") 778 779 ginkgo.By("updating") 780 var vapbUpdated *admissionregistrationv1.ValidatingAdmissionPolicyBinding 781 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 782 vap, err := client.Get(ctx, vapbCreated.Name, metav1.GetOptions{}) 783 framework.ExpectNoError(err) 784 785 vapbToUpdate := vap.DeepCopy() 786 vapbToUpdate.Annotations["updated"] = "true" 787 vapbToUpdate.Spec.ValidationActions = []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny} 788 789 vapbUpdated, err = client.Update(ctx, vapbToUpdate, metav1.UpdateOptions{}) 790 return err 791 }) 792 framework.ExpectNoError(err, "failed to update validatingadmissionpolicybinding %q", vapbCreated.Name) 793 gomega.Expect(vapbUpdated.Annotations).To(gomega.HaveKeyWithValue("updated", "true"), "updated object should have the applied annotation") 794 gomega.Expect(vapbUpdated.Spec.ValidationActions).To(gomega.Equal([]admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny}), "updated object should have the applied spec") 795 796 framework.Logf("waiting for watch events with expected annotations") 797 for sawAnnotation := false; !sawAnnotation; { 798 select { 799 case evt, ok := <-vapbWatch.ResultChan(): 800 if !ok { 801 framework.Fail("watch channel should not close") 802 } 803 gomega.Expect(evt.Type).To(gomega.Equal(watch.Modified)) 804 vapbWatched, isFS := evt.Object.(*admissionregistrationv1.ValidatingAdmissionPolicyBinding) 805 if !isFS { 806 framework.Failf("expected an object of type: %T, but got %T", &admissionregistrationv1.ValidatingAdmissionPolicyBinding{}, evt.Object) 807 } 808 if vapbWatched.Annotations["patched"] == "true" { 809 sawAnnotation = true 810 vapbWatch.Stop() 811 } else { 812 framework.Logf("missing expected annotations, waiting: %#v", vapbWatched.Annotations) 813 } 814 case <-time.After(wait.ForeverTestTimeout): 815 framework.Fail("timed out waiting for watch event") 816 } 817 } 818 ginkgo.By("deleting") 819 err = client.Delete(ctx, vapbCreated.Name, metav1.DeleteOptions{}) 820 framework.ExpectNoError(err) 821 vapbTmp, err := client.Get(ctx, vapbCreated.Name, metav1.GetOptions{}) 822 switch { 823 case err == nil && vapbTmp.GetDeletionTimestamp() != nil && len(vapbTmp.GetFinalizers()) > 0: 824 // deletion requested successfully, object is blocked by finalizers 825 case err == nil: 826 framework.Failf("expected deleted object, got %#v", vapbTmp) 827 case apierrors.IsNotFound(err): 828 // deleted successfully 829 default: 830 framework.Failf("expected 404, got %#v", err) 831 } 832 833 list, err = client.List(ctx, metav1.ListOptions{LabelSelector: label}) 834 var itemsWithoutFinalizer []admissionregistrationv1.ValidatingAdmissionPolicyBinding 835 for _, item := range list.Items { 836 if len(item.GetFinalizers()) == 0 { 837 itemsWithoutFinalizer = append(itemsWithoutFinalizer, item) 838 } 839 } 840 framework.ExpectNoError(err) 841 gomega.Expect(itemsWithoutFinalizer).To(gomega.HaveLen(2), "filtered list should have 2 items") 842 843 ginkgo.By("deleting a collection") 844 err = client.DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: label}) 845 framework.ExpectNoError(err) 846 847 list, err = client.List(ctx, metav1.ListOptions{LabelSelector: label}) 848 var itemsColWithoutFinalizer []admissionregistrationv1.ValidatingAdmissionPolicyBinding 849 for _, item := range list.Items { 850 if !(item.GetDeletionTimestamp() != nil && len(item.GetFinalizers()) > 0) { 851 itemsColWithoutFinalizer = append(itemsColWithoutFinalizer, item) 852 } 853 } 854 framework.ExpectNoError(err) 855 gomega.Expect(itemsColWithoutFinalizer).To(gomega.BeEmpty(), "filtered list should have 0 items") 856 }) 857 }) 858 859 func createBinding(bindingName string, uniqueLabel string, policyName string) *admissionregistrationv1.ValidatingAdmissionPolicyBinding { 860 return &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ 861 ObjectMeta: metav1.ObjectMeta{Name: bindingName}, 862 Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{ 863 PolicyName: policyName, 864 MatchResources: &admissionregistrationv1.MatchResources{ 865 NamespaceSelector: &metav1.LabelSelector{ 866 MatchLabels: map[string]string{uniqueLabel: "true"}, 867 }, 868 }, 869 ValidationActions: []admissionregistrationv1.ValidationAction{admissionregistrationv1.Deny}, 870 }, 871 } 872 } 873 874 func basicDeployment(name string, replicas int32) *appsv1.Deployment { 875 return &appsv1.Deployment{ 876 ObjectMeta: metav1.ObjectMeta{ 877 Name: name, 878 Labels: map[string]string{"app": "nginx"}, 879 }, 880 Spec: appsv1.DeploymentSpec{ 881 Replicas: &replicas, 882 Selector: &metav1.LabelSelector{ 883 MatchLabels: map[string]string{"app": "nginx"}, 884 }, 885 Template: v1.PodTemplateSpec{ 886 ObjectMeta: metav1.ObjectMeta{ 887 Labels: map[string]string{"app": "nginx"}, 888 }, 889 Spec: v1.PodSpec{ 890 Containers: []v1.Container{ 891 { 892 Name: "nginx", 893 Image: "nginx:latest", 894 }, 895 }, 896 }, 897 }, 898 }} 899 } 900 901 func basicReplicaSet(name string, replicas int32) *appsv1.ReplicaSet { 902 return &appsv1.ReplicaSet{ 903 ObjectMeta: metav1.ObjectMeta{ 904 Name: name, 905 Labels: map[string]string{"app": "nginx"}, 906 }, 907 Spec: appsv1.ReplicaSetSpec{ 908 Replicas: &replicas, 909 Selector: &metav1.LabelSelector{ 910 MatchLabels: map[string]string{"app": "nginx"}, 911 }, 912 Template: v1.PodTemplateSpec{ 913 ObjectMeta: metav1.ObjectMeta{ 914 Labels: map[string]string{"app": "nginx"}, 915 }, 916 Spec: v1.PodSpec{ 917 Containers: []v1.Container{ 918 { 919 Name: "nginx", 920 Image: "nginx:latest", 921 }, 922 }, 923 }, 924 }, 925 }} 926 } 927 928 type validatingAdmissionPolicyBuilder struct { 929 policy *admissionregistrationv1.ValidatingAdmissionPolicy 930 } 931 932 type resourceRuleBuilder struct { 933 policyBuilder *validatingAdmissionPolicyBuilder 934 resourceRule *admissionregistrationv1.NamedRuleWithOperations 935 } 936 937 func newValidatingAdmissionPolicyBuilder(policyName string) *validatingAdmissionPolicyBuilder { 938 return &validatingAdmissionPolicyBuilder{ 939 policy: &admissionregistrationv1.ValidatingAdmissionPolicy{ 940 ObjectMeta: metav1.ObjectMeta{Name: policyName}, 941 }, 942 } 943 } 944 945 func (b *validatingAdmissionPolicyBuilder) MatchUniqueNamespace(uniqueLabel string) *validatingAdmissionPolicyBuilder { 946 if b.policy.Spec.MatchConstraints == nil { 947 b.policy.Spec.MatchConstraints = &admissionregistrationv1.MatchResources{} 948 } 949 b.policy.Spec.MatchConstraints.NamespaceSelector = &metav1.LabelSelector{ 950 MatchLabels: map[string]string{ 951 uniqueLabel: "true", 952 }, 953 } 954 return b 955 } 956 957 func (b *validatingAdmissionPolicyBuilder) StartResourceRule() *resourceRuleBuilder { 958 return &resourceRuleBuilder{ 959 policyBuilder: b, 960 resourceRule: &admissionregistrationv1.NamedRuleWithOperations{ 961 RuleWithOperations: admissionregistrationv1.RuleWithOperations{ 962 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update}, 963 Rule: admissionregistrationv1.Rule{ 964 APIGroups: []string{"apps"}, 965 APIVersions: []string{"v1"}, 966 Resources: []string{"deployments"}, 967 }, 968 }, 969 }, 970 } 971 } 972 973 func (rb *resourceRuleBuilder) CreateAndUpdate() *resourceRuleBuilder { 974 rb.resourceRule.Operations = []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update} 975 return rb 976 } 977 978 func (rb *resourceRuleBuilder) MatchResource(groups []string, versions []string, resources []string) *resourceRuleBuilder { 979 rb.resourceRule.Rule = admissionregistrationv1.Rule{ 980 APIGroups: groups, 981 APIVersions: versions, 982 Resources: resources, 983 } 984 return rb 985 } 986 987 func (rb *resourceRuleBuilder) EndResourceRule() *validatingAdmissionPolicyBuilder { 988 b := rb.policyBuilder 989 if b.policy.Spec.MatchConstraints == nil { 990 b.policy.Spec.MatchConstraints = &admissionregistrationv1.MatchResources{} 991 } 992 b.policy.Spec.MatchConstraints.ResourceRules = append(b.policy.Spec.MatchConstraints.ResourceRules, *rb.resourceRule) 993 return b 994 } 995 996 func (b *validatingAdmissionPolicyBuilder) WithValidation(validation admissionregistrationv1.Validation) *validatingAdmissionPolicyBuilder { 997 b.policy.Spec.Validations = append(b.policy.Spec.Validations, validation) 998 return b 999 } 1000 1001 func (b *validatingAdmissionPolicyBuilder) WithVariable(variable admissionregistrationv1.Variable) *validatingAdmissionPolicyBuilder { 1002 b.policy.Spec.Variables = append(b.policy.Spec.Variables, variable) 1003 return b 1004 } 1005 1006 func (b *validatingAdmissionPolicyBuilder) Build() *admissionregistrationv1.ValidatingAdmissionPolicy { 1007 return b.policy 1008 } 1009 1010 func crontabExampleCRD() *apiextensionsv1.CustomResourceDefinition { 1011 return &apiextensionsv1.CustomResourceDefinition{ 1012 ObjectMeta: metav1.ObjectMeta{ 1013 Name: "crontabs.stable.example.com", 1014 }, 1015 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1016 Group: "stable.example.com", 1017 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1018 { 1019 Name: "v1", 1020 Served: true, 1021 Storage: true, 1022 Schema: &apiextensionsv1.CustomResourceValidation{ 1023 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1024 Type: "object", 1025 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1026 "spec": { 1027 Type: "object", 1028 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1029 "cronSpec": { 1030 Type: "string", 1031 }, 1032 "image": { 1033 Type: "string", 1034 }, 1035 "replicas": { 1036 Type: "integer", 1037 }, 1038 }, 1039 }, 1040 }, 1041 }}, 1042 }, 1043 }, 1044 Scope: apiextensionsv1.NamespaceScoped, 1045 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1046 Plural: "crontabs", 1047 Singular: "crontab", 1048 Kind: "CronTab", 1049 ShortNames: []string{"ct"}, 1050 }, 1051 }, 1052 } 1053 }