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