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 }