1
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
65
66
67
68 labelNamespace(ctx, f, f.Namespace.Name)
69 })
70
71
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
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 {
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'",
181 MessageExpression: "'wants replicas > 1, got ' + object.spec.replicas",
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 {
198 return true, nil
199 }
200 return false, nil
201 })
202 framework.ExpectNoError(err, "wait for type checking")
203
204
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
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
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
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'",
348 }).
349 WithValidation(admissionregistrationv1.Validation{
350 Expression: "object.spec.maxRetries < 10",
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
367
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
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
627 case err == nil:
628 framework.Failf("expected deleted object, got %#v", vapTmp)
629 case apierrors.IsNotFound(err):
630
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
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
825 case err == nil:
826 framework.Failf("expected deleted object, got %#v", vapbTmp)
827 case apierrors.IsNotFound(err):
828
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 }
1054
View as plain text