k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/apiserver/cel/admission_policy_test.go (about)

     1  /*
     2  Copyright 2023 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 cel
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/csv"
    23  	"sort"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	"k8s.io/api/admission/v1beta1"
    30  	corev1 "k8s.io/api/core/v1"
    31  	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    32  	genericfeatures "k8s.io/apiserver/pkg/features"
    33  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    34  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    35  
    36  	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    37  	"k8s.io/kubernetes/pkg/apis/admissionregistration"
    38  	admissionregistrationv1alpha1apis "k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1"
    39  	admissionregistrationv1beta1apis "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
    40  	"k8s.io/kubernetes/test/integration/etcd"
    41  	"k8s.io/kubernetes/test/integration/framework"
    42  
    43  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    44  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    45  	"k8s.io/apimachinery/pkg/runtime/schema"
    46  	"k8s.io/client-go/dynamic"
    47  	clientset "k8s.io/client-go/kubernetes"
    48  
    49  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    50  	admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
    51  	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
    52  )
    53  
    54  const (
    55  	beginSentinel   = "###___BEGIN_SENTINEL___###"
    56  	recordSeparator = `###$$$###`
    57  )
    58  
    59  // Policy registration helpers
    60  var testSpec admissionregistration.ValidatingAdmissionPolicy = admissionregistration.ValidatingAdmissionPolicy{
    61  	Spec: admissionregistration.ValidatingAdmissionPolicySpec{
    62  		ParamKind: &admissionregistration.ParamKind{
    63  			APIVersion: "v1",
    64  			Kind:       "ConfigMap",
    65  		},
    66  		Variables: []admissionregistration.Variable{
    67  			{
    68  				Name:       "shouldFail",
    69  				Expression: `true`,
    70  			},
    71  			{
    72  				Name:       "resourceGroup",
    73  				Expression: `has(request.resource.group) ? request.resource.group : ""`,
    74  			},
    75  			{
    76  				Name:       "resourceVersion",
    77  				Expression: `has(request.resource.version) ? request.resource.version : ""`,
    78  			},
    79  			{
    80  				Name:       "resourceResource",
    81  				Expression: `has(request.resource.resource) ? request.resource.resource : ""`,
    82  			},
    83  			{
    84  				Name:       "subresource",
    85  				Expression: `has(request.subResource) ? request.subResource : ""`,
    86  			},
    87  			{
    88  				Name:       "operation",
    89  				Expression: `has(request.operation) ? request.operation : ""`,
    90  			},
    91  			{
    92  				Name:       "name",
    93  				Expression: `has(request.name) ? request.name : ""`,
    94  			},
    95  			{
    96  				Name:       "namespaceName",
    97  				Expression: `has(request.namespace) ? request.namespace : ""`,
    98  			},
    99  			{
   100  				Name:       "objectExists",
   101  				Expression: `object != null ? "true" : "false"`,
   102  			},
   103  			{
   104  				Name:       "objectAPIVersion",
   105  				Expression: `(object != null && has(object.apiVersion)) ? object.apiVersion : ""`,
   106  			},
   107  			{
   108  				Name:       "objectKind",
   109  				Expression: `(object != null && has(object.kind)) ? object.kind : ""`,
   110  			},
   111  			{
   112  				Name:       "oldObjectExists",
   113  				Expression: `oldObject != null ? "true" : "false"`,
   114  			},
   115  			{
   116  				Name:       "oldObjectAPIVersion",
   117  				Expression: `(oldObject != null && has(oldObject.apiVersion)) ? oldObject.apiVersion : ""`,
   118  			},
   119  			{
   120  				Name:       "oldObjectKind",
   121  				Expression: `(oldObject != null && has(oldObject.kind)) ? oldObject.kind : ""`,
   122  			},
   123  			{
   124  				Name:       "optionsExists",
   125  				Expression: `(has(request.options) && request.options != null) ? "true" : "false"`,
   126  			},
   127  			{
   128  				Name:       "optionsKind",
   129  				Expression: `(has(request.options) && has(request.options.kind)) ? request.options.kind : ""`,
   130  			},
   131  			{
   132  				Name:       "optionsAPIVersion",
   133  				Expression: `(has(request.options) && has(request.options.apiVersion)) ? request.options.apiVersion : ""`,
   134  			},
   135  			{
   136  				Name:       "paramsPhase",
   137  				Expression: `params.data.phase`,
   138  			},
   139  			{
   140  				Name:       "paramsVersion",
   141  				Expression: `params.data.version`,
   142  			},
   143  			{
   144  				Name:       "paramsConvert",
   145  				Expression: `params.data.convert`,
   146  			},
   147  		},
   148  		// Would be nice to use CEL to create a single map
   149  		// and stringify it. Unfortunately those library functions
   150  		// are not yet available, so we must create a map
   151  		// like so
   152  		Validations: []admissionregistration.Validation{
   153  			{
   154  				// newlines forbidden so use recordSeparator
   155  				Expression:        "!variables.shouldFail",
   156  				MessageExpression: `"` + beginSentinel + `resourceGroup,resourceVersion,resourceResource,subresource,operation,name,namespace,objectExists,objectKind,objectAPIVersion,oldObjectExists,oldObjectKind,oldObjectAPIVersion,optionsExists,optionsKind,optionsAPIVersion,paramsPhase,paramsVersion,paramsConvert` + recordSeparator + `"+variables.resourceGroup + "," + variables.resourceVersion + "," + variables.resourceResource + "," + variables.subresource + "," + variables.operation + "," + variables.name + "," + variables.namespaceName + "," + variables.objectExists + "," + variables.objectKind + "," + variables.objectAPIVersion + "," + variables.oldObjectExists + "," + variables.oldObjectKind + "," + variables.oldObjectAPIVersion + "," + variables.optionsExists + "," + variables.optionsKind + "," + variables.optionsAPIVersion + "," + variables.paramsPhase + "," + variables.paramsVersion + "," + variables.paramsConvert`,
   157  			},
   158  		},
   159  		MatchConditions: []admissionregistration.MatchCondition{
   160  			{
   161  				Name:       "testclient-only",
   162  				Expression: `request.userInfo.username == "` + testClientUsername + `"`,
   163  			},
   164  			{
   165  				Name:       "ignore-test-config",
   166  				Expression: `object == null || !has(object.metadata) || !has(object.metadata.annotations) || !has(object.metadata.annotations.skipMatch) || object.metadata.annotations.skipMatch != "yes"`,
   167  			},
   168  		},
   169  	},
   170  }
   171  
   172  func createV1beta1ValidatingPolicyAndBinding(client clientset.Interface, convertedRules []admissionregistrationv1beta1.NamedRuleWithOperations) error {
   173  	denyAction := admissionregistrationv1beta1.DenyAction
   174  	exact := admissionregistrationv1beta1.Exact
   175  	equivalent := admissionregistrationv1beta1.Equivalent
   176  
   177  	var outSpec admissionregistrationv1beta1.ValidatingAdmissionPolicy
   178  	if err := admissionregistrationv1beta1apis.Convert_admissionregistration_ValidatingAdmissionPolicy_To_v1beta1_ValidatingAdmissionPolicy(&testSpec, &outSpec, nil); err != nil {
   179  		return err
   180  	}
   181  
   182  	exactPolicyTemplate := outSpec.DeepCopy()
   183  	convertedPolicyTemplate := outSpec.DeepCopy()
   184  
   185  	exactPolicyTemplate.SetName("test-policy-v1beta1")
   186  	exactPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
   187  		ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{
   188  			{
   189  				RuleWithOperations: admissionregistrationv1.RuleWithOperations{
   190  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
   191  					Rule:       admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
   192  				},
   193  			},
   194  		},
   195  		MatchPolicy: &exact,
   196  	}
   197  
   198  	convertedPolicyTemplate.SetName("test-policy-v1beta1-convert")
   199  	convertedPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{
   200  		ResourceRules: convertedRules,
   201  		MatchPolicy:   &equivalent,
   202  	}
   203  
   204  	exactPolicy, err := client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Create(context.TODO(), exactPolicyTemplate, metav1.CreateOptions{})
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	convertPolicy, err := client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Create(context.TODO(), convertedPolicyTemplate, metav1.CreateOptions{})
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	// Create a param that holds the options for this
   215  	configuration, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
   216  		ObjectMeta: metav1.ObjectMeta{
   217  			Name:      "test-policy-v1beta1-param",
   218  			Namespace: "default",
   219  			Annotations: map[string]string{
   220  				"skipMatch": "yes",
   221  			},
   222  		},
   223  		Data: map[string]string{
   224  			"version": "v1beta1",
   225  			"phase":   validation,
   226  			"convert": "false",
   227  		},
   228  	}, metav1.CreateOptions{})
   229  	if err != nil {
   230  		return err
   231  	}
   232  
   233  	configurationConvert, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
   234  		ObjectMeta: metav1.ObjectMeta{
   235  			Name:      "test-policy-v1beta1-convert-param",
   236  			Namespace: "default",
   237  			Annotations: map[string]string{
   238  				"skipMatch": "yes",
   239  			},
   240  		},
   241  		Data: map[string]string{
   242  			"version": "v1beta1",
   243  			"phase":   validation,
   244  			"convert": "true",
   245  		},
   246  	}, metav1.CreateOptions{})
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	_, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding{
   252  		ObjectMeta: metav1.ObjectMeta{
   253  			Name: "test-policy-v1beta1-binding",
   254  		},
   255  		Spec: admissionregistrationv1beta1.ValidatingAdmissionPolicyBindingSpec{
   256  			PolicyName:        exactPolicy.GetName(),
   257  			ValidationActions: []admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.Warn},
   258  			ParamRef: &admissionregistrationv1beta1.ParamRef{
   259  				Name:                    configuration.GetName(),
   260  				Namespace:               configuration.GetNamespace(),
   261  				ParameterNotFoundAction: &denyAction,
   262  			},
   263  		},
   264  	}, metav1.CreateOptions{})
   265  	if err != nil {
   266  		return err
   267  	}
   268  	_, err = client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding{
   269  		ObjectMeta: metav1.ObjectMeta{
   270  			Name: "test-policy-v1beta1-convert-binding",
   271  		},
   272  		Spec: admissionregistrationv1beta1.ValidatingAdmissionPolicyBindingSpec{
   273  			PolicyName:        convertPolicy.GetName(),
   274  			ValidationActions: []admissionregistrationv1beta1.ValidationAction{admissionregistrationv1beta1.Warn},
   275  			ParamRef: &admissionregistrationv1beta1.ParamRef{
   276  				Name:                    configurationConvert.GetName(),
   277  				Namespace:               configurationConvert.GetNamespace(),
   278  				ParameterNotFoundAction: &denyAction,
   279  			},
   280  		},
   281  	}, metav1.CreateOptions{})
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	return nil
   287  }
   288  
   289  func createV1alpha1ValidatingPolicyAndBinding(client clientset.Interface, convertedRules []admissionregistrationv1alpha1.NamedRuleWithOperations) error {
   290  	exact := admissionregistrationv1alpha1.Exact
   291  	equivalent := admissionregistrationv1alpha1.Equivalent
   292  	denyAction := admissionregistrationv1alpha1.DenyAction
   293  
   294  	var outSpec admissionregistrationv1alpha1.ValidatingAdmissionPolicy
   295  	if err := admissionregistrationv1alpha1apis.Convert_admissionregistration_ValidatingAdmissionPolicy_To_v1alpha1_ValidatingAdmissionPolicy(&testSpec, &outSpec, nil); err != nil {
   296  		return err
   297  	}
   298  
   299  	exactPolicyTemplate := outSpec.DeepCopy()
   300  	convertedPolicyTemplate := outSpec.DeepCopy()
   301  
   302  	exactPolicyTemplate.SetName("test-policy-v1alpha1")
   303  	exactPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
   304  		ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{
   305  			{
   306  				RuleWithOperations: admissionregistrationv1.RuleWithOperations{
   307  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
   308  					Rule:       admissionregistrationv1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
   309  				},
   310  			},
   311  		},
   312  		MatchPolicy: &exact,
   313  	}
   314  
   315  	convertedPolicyTemplate.SetName("test-policy-v1alpha1-convert")
   316  	convertedPolicyTemplate.Spec.MatchConstraints = &admissionregistrationv1alpha1.MatchResources{
   317  		ResourceRules: convertedRules,
   318  		MatchPolicy:   &equivalent,
   319  	}
   320  
   321  	exactPolicy, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), exactPolicyTemplate, metav1.CreateOptions{})
   322  	if err != nil {
   323  		return err
   324  	}
   325  
   326  	convertPolicy, err := client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(context.TODO(), convertedPolicyTemplate, metav1.CreateOptions{})
   327  	if err != nil {
   328  		return err
   329  	}
   330  
   331  	// Create a param that holds the options for this
   332  	configuration, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
   333  		ObjectMeta: metav1.ObjectMeta{
   334  			Name:      "test-policy-v1alpha1-param",
   335  			Namespace: "default",
   336  			Annotations: map[string]string{
   337  				"skipMatch": "yes",
   338  			},
   339  		},
   340  		Data: map[string]string{
   341  			"version": "v1alpha1",
   342  			"phase":   validation,
   343  			"convert": "false",
   344  		},
   345  	}, metav1.CreateOptions{})
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	configurationConvert, err := client.CoreV1().ConfigMaps("default").Create(context.TODO(), &corev1.ConfigMap{
   351  		ObjectMeta: metav1.ObjectMeta{
   352  			Name:      "test-policy-v1alpha1-convert-param",
   353  			Namespace: "default",
   354  			Annotations: map[string]string{
   355  				"skipMatch": "yes",
   356  			},
   357  		},
   358  		Data: map[string]string{
   359  			"version": "v1alpha1",
   360  			"phase":   validation,
   361  			"convert": "true",
   362  		},
   363  	}, metav1.CreateOptions{})
   364  	if err != nil {
   365  		return err
   366  	}
   367  
   368  	_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding{
   369  		ObjectMeta: metav1.ObjectMeta{
   370  			Name: "test-policy-v1alpha1-binding",
   371  		},
   372  		Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicyBindingSpec{
   373  			PolicyName:        exactPolicy.GetName(),
   374  			ValidationActions: []admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Warn},
   375  			ParamRef: &admissionregistrationv1alpha1.ParamRef{
   376  				Name:                    configuration.GetName(),
   377  				Namespace:               configuration.GetNamespace(),
   378  				ParameterNotFoundAction: &denyAction,
   379  			},
   380  		},
   381  	}, metav1.CreateOptions{})
   382  	if err != nil {
   383  		return err
   384  	}
   385  	_, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicyBindings().Create(context.TODO(), &admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding{
   386  		ObjectMeta: metav1.ObjectMeta{
   387  			Name: "test-policy-v1alpha1-convert-binding",
   388  		},
   389  		Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicyBindingSpec{
   390  			PolicyName:        convertPolicy.GetName(),
   391  			ValidationActions: []admissionregistrationv1alpha1.ValidationAction{admissionregistrationv1alpha1.Warn},
   392  			ParamRef: &admissionregistrationv1alpha1.ParamRef{
   393  				Name:                    configurationConvert.GetName(),
   394  				Namespace:               configurationConvert.GetNamespace(),
   395  				ParameterNotFoundAction: &denyAction,
   396  			},
   397  		},
   398  	}, metav1.CreateOptions{})
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	return nil
   404  }
   405  
   406  // This test shows that policy intercepts all requests for all resources,
   407  // subresources, verbs, and input versions of policy/binding.
   408  //
   409  // This test tries to mirror very closely the same test for webhook admission
   410  // test/integration/apiserver/admissionwebhook/admission_test.go testWebhookAdmission
   411  func TestPolicyAdmission(t *testing.T) {
   412  	featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)
   413  
   414  	holder := &policyExpectationHolder{
   415  		holder: holder{
   416  			t:                 t,
   417  			gvrToConvertedGVR: map[metav1.GroupVersionResource]metav1.GroupVersionResource{},
   418  			gvrToConvertedGVK: map[metav1.GroupVersionResource]schema.GroupVersionKind{},
   419  		},
   420  	}
   421  
   422  	server := apiservertesting.StartTestServerOrDie(t, nil, []string{
   423  		"--enable-admission-plugins", "ValidatingAdmissionPolicy",
   424  		// turn off admission plugins that add finalizers
   425  		"--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
   426  		// force enable all resources so we can check storage.
   427  		"--runtime-config=api/all=true",
   428  	}, framework.SharedEtcd())
   429  	defer server.TearDownFn()
   430  
   431  	// Create admission policy & binding that match everything
   432  	clientConfig := server.ClientConfig
   433  	clientConfig.Impersonate.UserName = testClientUsername
   434  	clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
   435  	clientConfig.WarningHandler = holder
   436  	client, err := clientset.NewForConfig(clientConfig)
   437  	if err != nil {
   438  		t.Fatal(err)
   439  	}
   440  
   441  	// create CRDs
   442  	etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
   443  
   444  	if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
   445  		t.Fatal(err)
   446  	}
   447  
   448  	// gather resources to test
   449  	dynamicClient, err := dynamic.NewForConfig(clientConfig)
   450  	if err != nil {
   451  		t.Fatal(err)
   452  	}
   453  
   454  	_, resources, err := client.Discovery().ServerGroupsAndResources()
   455  	if err != nil {
   456  		t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
   457  	}
   458  
   459  	gvrsToTest := []schema.GroupVersionResource{}
   460  	resourcesByGVR := map[schema.GroupVersionResource]metav1.APIResource{}
   461  
   462  	for _, list := range resources {
   463  		defaultGroupVersion, err := schema.ParseGroupVersion(list.GroupVersion)
   464  		if err != nil {
   465  			t.Errorf("Failed to get GroupVersion for: %+v", list)
   466  			continue
   467  		}
   468  		for _, resource := range list.APIResources {
   469  			if resource.Group == "" {
   470  				resource.Group = defaultGroupVersion.Group
   471  			}
   472  			if resource.Version == "" {
   473  				resource.Version = defaultGroupVersion.Version
   474  			}
   475  			gvr := defaultGroupVersion.WithResource(resource.Name)
   476  			resourcesByGVR[gvr] = resource
   477  			if shouldTestResource(gvr, resource) {
   478  				gvrsToTest = append(gvrsToTest, gvr)
   479  			}
   480  		}
   481  	}
   482  
   483  	sort.SliceStable(gvrsToTest, func(i, j int) bool {
   484  		if gvrsToTest[i].Group < gvrsToTest[j].Group {
   485  			return true
   486  		}
   487  		if gvrsToTest[i].Group > gvrsToTest[j].Group {
   488  			return false
   489  		}
   490  		if gvrsToTest[i].Version < gvrsToTest[j].Version {
   491  			return true
   492  		}
   493  		if gvrsToTest[i].Version > gvrsToTest[j].Version {
   494  			return false
   495  		}
   496  		if gvrsToTest[i].Resource < gvrsToTest[j].Resource {
   497  			return true
   498  		}
   499  		if gvrsToTest[i].Resource > gvrsToTest[j].Resource {
   500  			return false
   501  		}
   502  		return true
   503  	})
   504  
   505  	// map unqualified resource names to the fully qualified resource we will expect to be converted to
   506  	// Note: this only works because there are no overlapping resource names in-process that are not co-located
   507  	convertedResources := map[string]schema.GroupVersionResource{}
   508  	// build the webhook rules enumerating the specific group/version/resources we want
   509  	convertedV1beta1Rules := []admissionregistrationv1beta1.NamedRuleWithOperations{}
   510  	convertedV1alpha1Rules := []admissionregistrationv1alpha1.NamedRuleWithOperations{}
   511  	for _, gvr := range gvrsToTest {
   512  		metaGVR := metav1.GroupVersionResource{Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource}
   513  
   514  		convertedGVR, ok := convertedResources[gvr.Resource]
   515  		if !ok {
   516  			// this is the first time we've seen this resource
   517  			// record the fully qualified resource we expect
   518  			convertedGVR = gvr
   519  			convertedResources[gvr.Resource] = gvr
   520  			// add an admission rule indicating we can receive this version
   521  			convertedV1beta1Rules = append(convertedV1beta1Rules, admissionregistrationv1beta1.NamedRuleWithOperations{
   522  				RuleWithOperations: admissionregistrationv1.RuleWithOperations{
   523  					Operations: []admissionregistrationv1beta1.OperationType{admissionregistrationv1beta1.OperationAll},
   524  					Rule:       admissionregistrationv1beta1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
   525  				},
   526  			})
   527  			convertedV1alpha1Rules = append(convertedV1alpha1Rules, admissionregistrationv1alpha1.NamedRuleWithOperations{
   528  				RuleWithOperations: admissionregistrationv1.RuleWithOperations{
   529  					Operations: []admissionregistrationv1alpha1.OperationType{admissionregistrationv1alpha1.OperationAll},
   530  					Rule:       admissionregistrationv1alpha1.Rule{APIGroups: []string{gvr.Group}, APIVersions: []string{gvr.Version}, Resources: []string{gvr.Resource}},
   531  				},
   532  			})
   533  		}
   534  
   535  		// record the expected resource and kind
   536  		holder.gvrToConvertedGVR[metaGVR] = metav1.GroupVersionResource{Group: convertedGVR.Group, Version: convertedGVR.Version, Resource: convertedGVR.Resource}
   537  		holder.gvrToConvertedGVK[metaGVR] = schema.GroupVersionKind{Group: resourcesByGVR[convertedGVR].Group, Version: resourcesByGVR[convertedGVR].Version, Kind: resourcesByGVR[convertedGVR].Kind}
   538  	}
   539  
   540  	if err := createV1alpha1ValidatingPolicyAndBinding(client, convertedV1alpha1Rules); err != nil {
   541  		t.Fatal(err)
   542  	}
   543  
   544  	if err := createV1beta1ValidatingPolicyAndBinding(client, convertedV1beta1Rules); err != nil {
   545  		t.Fatal(err)
   546  	}
   547  
   548  	// Allow the policy & binding to establish
   549  	time.Sleep(1 * time.Second)
   550  
   551  	start := time.Now()
   552  	count := 0
   553  
   554  	// Test admission on all resources, subresources, and verbs
   555  	for _, gvr := range gvrsToTest {
   556  		resource := resourcesByGVR[gvr]
   557  		t.Run(gvr.Group+"."+gvr.Version+"."+strings.ReplaceAll(resource.Name, "/", "."), func(t *testing.T) {
   558  			for _, verb := range []string{"create", "update", "patch", "connect", "delete", "deletecollection"} {
   559  				if shouldTestResourceVerb(gvr, resource, verb) {
   560  					t.Run(verb, func(t *testing.T) {
   561  						count++
   562  						holder.reset(t)
   563  						testFunc := getTestFunc(gvr, verb)
   564  						testFunc(&testContext{
   565  							t:               t,
   566  							admissionHolder: holder,
   567  							client:          dynamicClient,
   568  							clientset:       client,
   569  							verb:            verb,
   570  							gvr:             gvr,
   571  							resource:        resource,
   572  							resources:       resourcesByGVR,
   573  						})
   574  						holder.verify(t)
   575  					})
   576  				}
   577  			}
   578  		})
   579  	}
   580  
   581  	if count >= 10 {
   582  		duration := time.Since(start)
   583  		perResourceDuration := time.Duration(int(duration) / count)
   584  		if perResourceDuration >= 150*time.Millisecond {
   585  			t.Errorf("expected resources to process in < 150ms, average was %v", perResourceDuration)
   586  		}
   587  	}
   588  }
   589  
   590  // Policy admission holder for test framework
   591  
   592  type policyExpectationHolder struct {
   593  	holder
   594  	warningLock sync.Mutex
   595  	warnings    []string
   596  }
   597  
   598  func (p *policyExpectationHolder) reset(t *testing.T) {
   599  	p.warningLock.Lock()
   600  	defer p.warningLock.Unlock()
   601  	p.warnings = nil
   602  
   603  	p.holder.reset(t)
   604  
   605  }
   606  func (p *policyExpectationHolder) expect(gvr schema.GroupVersionResource, gvk, optionsGVK schema.GroupVersionKind, operation v1beta1.Operation, name, namespace string, object, oldObject, options bool) {
   607  	p.holder.expect(gvr, gvk, optionsGVK, operation, name, namespace, object, oldObject, options)
   608  
   609  	p.lock.Lock()
   610  	defer p.lock.Unlock()
   611  	// Set up the recorded map with nil records for all combinations
   612  	p.recorded = map[webhookOptions]*admissionRequest{}
   613  	for _, phase := range []string{validation} {
   614  		for _, converted := range []bool{true, false} {
   615  			for _, version := range []string{"v1alpha1", "v1beta1"} {
   616  				p.recorded[webhookOptions{version: version, phase: phase, converted: converted}] = nil
   617  			}
   618  		}
   619  	}
   620  }
   621  
   622  func (p *policyExpectationHolder) verify(t *testing.T) {
   623  	p.warningLock.Lock()
   624  	defer p.warningLock.Unlock()
   625  
   626  	// Process all detected warnings and record in the nested handler
   627  	for _, w := range p.warnings {
   628  		var currentRequest *admissionRequest
   629  		var currentParams webhookOptions
   630  		if idx := strings.Index(w, beginSentinel); idx >= 0 {
   631  
   632  			csvData := strings.ReplaceAll(w[idx+len(beginSentinel):], recordSeparator, "\n")
   633  
   634  			b := bytes.Buffer{}
   635  			b.WriteString(csvData)
   636  			reader := csv.NewReader(&b)
   637  			csvRecords, err := reader.ReadAll()
   638  			if err != nil {
   639  				t.Fatal(err)
   640  				return
   641  			}
   642  
   643  			mappedCSV := []map[string]string{}
   644  			var header []string
   645  			for line, record := range csvRecords {
   646  				if line == 0 {
   647  					header = record
   648  				} else {
   649  					line := map[string]string{}
   650  					for i := 0; i < len(record); i++ {
   651  						line[header[i]] = record[i]
   652  					}
   653  					mappedCSV = append(mappedCSV, line)
   654  				}
   655  			}
   656  
   657  			if len(mappedCSV) != 1 {
   658  				t.Fatal("incorrect # CSV elements in parsed warning")
   659  				return
   660  			}
   661  
   662  			data := mappedCSV[0]
   663  			currentRequest = &admissionRequest{
   664  				Operation: data["operation"],
   665  				Name:      data["name"],
   666  				Namespace: data["namespace"],
   667  				Resource: metav1.GroupVersionResource{
   668  					Group:    data["resourceGroup"],
   669  					Version:  data["resourceVersion"],
   670  					Resource: data["resourceResource"],
   671  				},
   672  				SubResource: data["subresource"],
   673  			}
   674  			currentParams = webhookOptions{
   675  				version:   data["paramsVersion"],
   676  				phase:     data["paramsPhase"],
   677  				converted: data["paramsConvert"] == "true",
   678  			}
   679  
   680  			if e, ok := data["objectExists"]; ok && e == "true" {
   681  				currentRequest.Object.Object = &unstructured.Unstructured{}
   682  				currentRequest.Object.Object.(*unstructured.Unstructured).SetAPIVersion(data["objectAPIVersion"])
   683  				currentRequest.Object.Object.(*unstructured.Unstructured).SetKind(data["objectKind"])
   684  			}
   685  
   686  			if e, ok := data["oldObjectExists"]; ok && e == "true" {
   687  				currentRequest.OldObject.Object = &unstructured.Unstructured{}
   688  				currentRequest.OldObject.Object.(*unstructured.Unstructured).SetAPIVersion(data["oldObjectAPIVersion"])
   689  				currentRequest.OldObject.Object.(*unstructured.Unstructured).SetKind(data["oldObjectKind"])
   690  			}
   691  
   692  			if e, ok := data["optionsExists"]; ok && e == "true" {
   693  				currentRequest.Options.Object = &unstructured.Unstructured{}
   694  				currentRequest.Options.Object.(*unstructured.Unstructured).SetAPIVersion(data["optionsAPIVersion"])
   695  				currentRequest.Options.Object.(*unstructured.Unstructured).SetKind(data["optionsKind"])
   696  			}
   697  
   698  			p.holder.record(currentParams.version, currentParams.phase, currentParams.converted, currentRequest)
   699  		}
   700  	}
   701  
   702  	p.holder.verify(t)
   703  }
   704  
   705  func (p *policyExpectationHolder) HandleWarningHeader(code int, agent string, message string) {
   706  	if code != 299 || len(message) == 0 {
   707  		return
   708  	}
   709  	p.warningLock.Lock()
   710  	defer p.warningLock.Unlock()
   711  	p.warnings = append(p.warnings, message)
   712  }