...

Source file src/k8s.io/kubernetes/test/e2e/apimachinery/validatingadmissionpolicy.go

Documentation: k8s.io/kubernetes/test/e2e/apimachinery

     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  }
  1054  

View as plain text