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 }