@@ -22,6 +22,7 @@ import (
22
22
"crypto/x509"
23
23
"encoding/json"
24
24
"io"
25
+ "k8s.io/apimachinery/pkg/util/version"
25
26
"net/http"
26
27
"net/http/httptest"
27
28
"strconv"
@@ -111,7 +112,6 @@ func newMatchConditionHandler(recorder *admissionRecorder) http.Handler {
111
112
112
113
// TestMatchConditions tests ValidatingWebhookConfigurations and MutatingWebhookConfigurations that validates different cases of matchCondition fields
113
114
func TestMatchConditions (t * testing.T ) {
114
- featuregatetesting .SetFeatureGateDuringTest (t , utilfeature .DefaultFeatureGate , genericfeatures .StrictCostEnforcementForWebhooks , false )
115
115
fail := admissionregistrationv1 .Fail
116
116
ignore := admissionregistrationv1 .Ignore
117
117
@@ -289,6 +289,296 @@ func TestMatchConditions(t *testing.T) {
289
289
matchConditionsTestPod ("test2" , "default" ),
290
290
},
291
291
},
292
+ }
293
+
294
+ roots := x509 .NewCertPool ()
295
+ if ! roots .AppendCertsFromPEM (localhostCert ) {
296
+ t .Fatal ("Failed to append Cert from PEM" )
297
+ }
298
+ cert , err := tls .X509KeyPair (localhostCert , localhostKey )
299
+ if err != nil {
300
+ t .Fatalf ("Failed to build cert with error: %+v" , err )
301
+ }
302
+
303
+ recorder := & admissionRecorder {requests : []* admissionv1.AdmissionRequest {}}
304
+
305
+ webhookServer := httptest .NewUnstartedServer (newMatchConditionHandler (recorder ))
306
+ webhookServer .TLS = & tls.Config {
307
+ RootCAs : roots ,
308
+ Certificates : []tls.Certificate {cert },
309
+ }
310
+ webhookServer .StartTLS ()
311
+ defer webhookServer .Close ()
312
+
313
+ dryRunCreate := metav1.CreateOptions {
314
+ DryRun : []string {metav1 .DryRunAll },
315
+ }
316
+
317
+ for _ , testcase := range testcases {
318
+ t .Run (testcase .name , func (t * testing.T ) {
319
+ upCh := recorder .Reset ()
320
+
321
+ server , err := apiservertesting .StartTestServer (t , nil , []string {
322
+ "--disable-admission-plugins=ServiceAccount" ,
323
+ }, framework .SharedEtcd ())
324
+ if err != nil {
325
+ t .Fatal (err )
326
+ }
327
+ defer server .TearDownFn ()
328
+
329
+ config := server .ClientConfig
330
+
331
+ client , err := clientset .NewForConfig (config )
332
+ if err != nil {
333
+ t .Fatal (err )
334
+ }
335
+
336
+ // Write markers to a separate namespace to avoid cross-talk
337
+ markerNs := "marker"
338
+ _ , err = client .CoreV1 ().Namespaces ().Create (context .TODO (), & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {Name : markerNs }}, metav1.CreateOptions {})
339
+ if err != nil {
340
+ t .Fatal (err )
341
+ }
342
+
343
+ // Create a marker object to use to check for the webhook configurations to be ready.
344
+ marker , err := client .CoreV1 ().Pods (markerNs ).Create (context .TODO (), newMarkerPod (markerNs ), metav1.CreateOptions {})
345
+ if err != nil {
346
+ t .Fatal (err )
347
+ }
348
+
349
+ endpoint := webhookServer .URL
350
+ markerEndpoint := webhookServer .URL + "/marker"
351
+ validatingwebhook := & admissionregistrationv1.ValidatingWebhookConfiguration {
352
+ ObjectMeta : metav1.ObjectMeta {
353
+ Name : "admission.integration.test" ,
354
+ },
355
+ Webhooks : []admissionregistrationv1.ValidatingWebhook {
356
+ {
357
+ Name : "admission.integration.test" ,
358
+ Rules : []admissionregistrationv1.RuleWithOperations {{
359
+ Operations : []admissionregistrationv1.OperationType {admissionregistrationv1 .Create },
360
+ Rule : admissionregistrationv1.Rule {
361
+ APIGroups : []string {"" },
362
+ APIVersions : []string {"v1" },
363
+ Resources : []string {"pods" },
364
+ },
365
+ }},
366
+ ClientConfig : admissionregistrationv1.WebhookClientConfig {
367
+ URL : & endpoint ,
368
+ CABundle : localhostCert ,
369
+ },
370
+ // ignore pods in the marker namespace
371
+ NamespaceSelector : & metav1.LabelSelector {
372
+ MatchExpressions : []metav1.LabelSelectorRequirement {
373
+ {
374
+ Key : corev1 .LabelMetadataName ,
375
+ Operator : metav1 .LabelSelectorOpNotIn ,
376
+ Values : []string {"marker" },
377
+ },
378
+ }},
379
+ FailurePolicy : testcase .failPolicy ,
380
+ SideEffects : & noSideEffects ,
381
+ AdmissionReviewVersions : []string {"v1" },
382
+ MatchConditions : testcase .matchConditions ,
383
+ },
384
+ {
385
+ Name : "admission.integration.test.marker" ,
386
+ Rules : []admissionregistrationv1.RuleWithOperations {{
387
+ Operations : []admissionregistrationv1.OperationType {admissionregistrationv1 .OperationAll },
388
+ Rule : admissionregistrationv1.Rule {APIGroups : []string {"" }, APIVersions : []string {"v1" }, Resources : []string {"pods" }},
389
+ }},
390
+ ClientConfig : admissionregistrationv1.WebhookClientConfig {
391
+ URL : & markerEndpoint ,
392
+ CABundle : localhostCert ,
393
+ },
394
+ NamespaceSelector : & metav1.LabelSelector {MatchLabels : map [string ]string {
395
+ corev1 .LabelMetadataName : "marker" ,
396
+ }},
397
+ ObjectSelector : & metav1.LabelSelector {MatchLabels : map [string ]string {"marker" : "true" }},
398
+ FailurePolicy : testcase .failPolicy ,
399
+ SideEffects : & noSideEffects ,
400
+ AdmissionReviewVersions : []string {"v1" },
401
+ },
402
+ },
403
+ }
404
+
405
+ validatingcfg , err := client .AdmissionregistrationV1 ().ValidatingWebhookConfigurations ().Create (context .TODO (), validatingwebhook , metav1.CreateOptions {})
406
+ if err != nil {
407
+ t .Fatal (err )
408
+ }
409
+
410
+ vhwHasBeenCleanedUp := false
411
+ defer func () {
412
+ if ! vhwHasBeenCleanedUp {
413
+ err := client .AdmissionregistrationV1 ().ValidatingWebhookConfigurations ().Delete (context .TODO (), validatingcfg .GetName (), metav1.DeleteOptions {})
414
+ if err != nil {
415
+ t .Fatal (err )
416
+ }
417
+ }
418
+ }()
419
+
420
+ // wait until new webhook is called the first time
421
+ if err := wait .PollUntilContextTimeout (context .Background (), time .Millisecond * 5 , wait .ForeverTestTimeout , true , func (_ context.Context ) (bool , error ) {
422
+ _ , err = client .CoreV1 ().Pods (markerNs ).Patch (context .TODO (), marker .Name , types .JSONPatchType , []byte ("[]" ), metav1.PatchOptions {})
423
+ select {
424
+ case <- upCh :
425
+ return true , nil
426
+ default :
427
+ t .Logf ("Waiting for webhook to become effective, getting marker object: %v" , err )
428
+ return false , nil
429
+ }
430
+ }); err != nil {
431
+ t .Fatal (err )
432
+ }
433
+
434
+ for _ , pod := range testcase .pods {
435
+ _ , err := client .CoreV1 ().Pods (pod .Namespace ).Create (context .TODO (), pod , dryRunCreate )
436
+ if ! testcase .expectErrorPod && err != nil {
437
+ t .Fatalf ("unexpected error creating test pod: %v" , err )
438
+ } else if testcase .expectErrorPod && err == nil {
439
+ t .Fatal ("expected error creating pods" )
440
+ } else if testcase .expectErrorPod && err != nil && ! strings .Contains (err .Error (), testcase .errMessage ) {
441
+ t .Fatalf ("expected error message includes: %v, but get %v" , testcase .errMessage , err )
442
+ }
443
+ }
444
+
445
+ if len (recorder .requests ) != len (testcase .matchedPods ) {
446
+ t .Errorf ("unexpected requests %v, expected %v" , recorder .requests , testcase .matchedPods )
447
+ }
448
+
449
+ for i , request := range recorder .requests {
450
+ if request .Name != testcase .matchedPods [i ].Name {
451
+ t .Errorf ("unexpected pod name %v, expected %v" , request .Name , testcase .matchedPods [i ].Name )
452
+ }
453
+ if request .Namespace != testcase .matchedPods [i ].Namespace {
454
+ t .Errorf ("unexpected pod namespace %v, expected %v" , request .Namespace , testcase .matchedPods [i ].Namespace )
455
+ }
456
+ }
457
+
458
+ // Reset and rerun against mutating webhook configuration
459
+ // TODO: private helper function for validation after creating vwh or mwh
460
+ upCh = recorder .Reset ()
461
+ err = client .AdmissionregistrationV1 ().ValidatingWebhookConfigurations ().Delete (context .TODO (), validatingcfg .GetName (), metav1.DeleteOptions {})
462
+ if err != nil {
463
+ t .Fatal (err )
464
+ } else {
465
+ vhwHasBeenCleanedUp = true
466
+ }
467
+
468
+ mutatingwebhook := & admissionregistrationv1.MutatingWebhookConfiguration {
469
+ ObjectMeta : metav1.ObjectMeta {
470
+ Name : "admission.integration.test" ,
471
+ },
472
+ Webhooks : []admissionregistrationv1.MutatingWebhook {
473
+ {
474
+ Name : "admission.integration.test" ,
475
+ Rules : []admissionregistrationv1.RuleWithOperations {{
476
+ Operations : []admissionregistrationv1.OperationType {admissionregistrationv1 .Create },
477
+ Rule : admissionregistrationv1.Rule {
478
+ APIGroups : []string {"" },
479
+ APIVersions : []string {"v1" },
480
+ Resources : []string {"pods" },
481
+ },
482
+ }},
483
+ ClientConfig : admissionregistrationv1.WebhookClientConfig {
484
+ URL : & endpoint ,
485
+ CABundle : localhostCert ,
486
+ },
487
+ // ignore pods in the marker namespace
488
+ NamespaceSelector : & metav1.LabelSelector {
489
+ MatchExpressions : []metav1.LabelSelectorRequirement {
490
+ {
491
+ Key : corev1 .LabelMetadataName ,
492
+ Operator : metav1 .LabelSelectorOpNotIn ,
493
+ Values : []string {"marker" },
494
+ },
495
+ }},
496
+ FailurePolicy : testcase .failPolicy ,
497
+ SideEffects : & noSideEffects ,
498
+ AdmissionReviewVersions : []string {"v1" },
499
+ MatchConditions : testcase .matchConditions ,
500
+ },
501
+ {
502
+ Name : "admission.integration.test.marker" ,
503
+ Rules : []admissionregistrationv1.RuleWithOperations {{
504
+ Operations : []admissionregistrationv1.OperationType {admissionregistrationv1 .OperationAll },
505
+ Rule : admissionregistrationv1.Rule {APIGroups : []string {"" }, APIVersions : []string {"v1" }, Resources : []string {"pods" }},
506
+ }},
507
+ ClientConfig : admissionregistrationv1.WebhookClientConfig {
508
+ URL : & markerEndpoint ,
509
+ CABundle : localhostCert ,
510
+ },
511
+ NamespaceSelector : & metav1.LabelSelector {MatchLabels : map [string ]string {
512
+ corev1 .LabelMetadataName : "marker" ,
513
+ }},
514
+ ObjectSelector : & metav1.LabelSelector {MatchLabels : map [string ]string {"marker" : "true" }},
515
+ FailurePolicy : testcase .failPolicy ,
516
+ SideEffects : & noSideEffects ,
517
+ AdmissionReviewVersions : []string {"v1" },
518
+ },
519
+ },
520
+ }
521
+
522
+ mutatingcfg , err := client .AdmissionregistrationV1 ().MutatingWebhookConfigurations ().Create (context .TODO (), mutatingwebhook , metav1.CreateOptions {})
523
+ if err != nil {
524
+ t .Fatal (err )
525
+ }
526
+ defer func () {
527
+ err := client .AdmissionregistrationV1 ().MutatingWebhookConfigurations ().Delete (context .TODO (), mutatingcfg .GetName (), metav1.DeleteOptions {})
528
+ if err != nil {
529
+ t .Fatal (err )
530
+ }
531
+ }()
532
+
533
+ // wait until new webhook is called the first time
534
+ if err := wait .PollUntilContextTimeout (context .Background (), time .Millisecond * 5 , wait .ForeverTestTimeout , true , func (_ context.Context ) (bool , error ) {
535
+ _ , err = client .CoreV1 ().Pods (markerNs ).Patch (context .TODO (), marker .Name , types .JSONPatchType , []byte ("[]" ), metav1.PatchOptions {})
536
+ select {
537
+ case <- upCh :
538
+ return true , nil
539
+ default :
540
+ t .Logf ("Waiting for webhook to become effective, getting marker object: %v" , err )
541
+ return false , nil
542
+ }
543
+ }); err != nil {
544
+ t .Fatal (err )
545
+ }
546
+
547
+ for _ , pod := range testcase .pods {
548
+ _ , err = client .CoreV1 ().Pods (pod .Namespace ).Create (context .TODO (), pod , dryRunCreate )
549
+ if testcase .expectErrorPod == false && err != nil {
550
+ t .Fatalf ("unexpected error creating test pod: %v" , err )
551
+ } else if testcase .expectErrorPod == true && err == nil {
552
+ t .Fatal ("expected error creating pods" )
553
+ }
554
+ }
555
+
556
+ if len (recorder .requests ) != len (testcase .matchedPods ) {
557
+ t .Errorf ("unexpected requests %v, expected %v" , recorder .requests , testcase .matchedPods )
558
+ }
559
+
560
+ for i , request := range recorder .requests {
561
+ if request .Name != testcase .matchedPods [i ].Name {
562
+ t .Errorf ("unexpected pod name %v, expected %v" , request .Name , testcase .matchedPods [i ].Name )
563
+ }
564
+ if request .Namespace != testcase .matchedPods [i ].Namespace {
565
+ t .Errorf ("unexpected pod namespace %v, expected %v" , request .Namespace , testcase .matchedPods [i ].Namespace )
566
+ }
567
+ }
568
+ })
569
+ }
570
+ }
571
+
572
+ func TestMatchConditionsWithoutStrictCostEnforcement (t * testing.T ) {
573
+ testcases := []struct {
574
+ name string
575
+ matchConditions []admissionregistrationv1.MatchCondition
576
+ pods []* corev1.Pod
577
+ matchedPods []* corev1.Pod
578
+ expectErrorPod bool
579
+ failPolicy * admissionregistrationv1.FailurePolicyType
580
+ errMessage string
581
+ }{
292
582
{
293
583
name : "without strict cost enforcement: Authz check does not exceed per call limit" ,
294
584
matchConditions : []admissionregistrationv1.MatchCondition {
@@ -303,7 +593,6 @@ func TestMatchConditions(t *testing.T) {
303
593
matchedPods : []* corev1.Pod {
304
594
matchConditionsTestPod ("test1" , "kube-system" ),
305
595
},
306
- failPolicy : & fail ,
307
596
expectErrorPod : false ,
308
597
},
309
598
{
@@ -315,7 +604,6 @@ func TestMatchConditions(t *testing.T) {
315
604
matchedPods : []* corev1.Pod {
316
605
matchConditionsTestPod ("test1" , "kube-system" ),
317
606
},
318
- failPolicy : & fail ,
319
607
expectErrorPod : false ,
320
608
},
321
609
}
@@ -346,8 +634,9 @@ func TestMatchConditions(t *testing.T) {
346
634
for _ , testcase := range testcases {
347
635
t .Run (testcase .name , func (t * testing.T ) {
348
636
upCh := recorder .Reset ()
349
-
350
- server , err := apiservertesting .StartTestServer (t , nil , []string {
637
+ featuregatetesting .SetFeatureGateEmulationVersionDuringTest (t , utilfeature .DefaultFeatureGate , version .MustParse ("1.31" ))
638
+ featuregatetesting .SetFeatureGateDuringTest (t , utilfeature .DefaultFeatureGate , genericfeatures .StrictCostEnforcementForWebhooks , false )
639
+ server , err := apiservertesting .StartTestServer (t , & apiservertesting.TestServerInstanceOptions {EmulationVersion : "1.31" }, []string {
351
640
"--disable-admission-plugins=ServiceAccount" ,
352
641
}, framework .SharedEtcd ())
353
642
if err != nil {
@@ -447,7 +736,7 @@ func TestMatchConditions(t *testing.T) {
447
736
}()
448
737
449
738
// wait until new webhook is called the first time
450
- if err := wait .PollImmediate ( time .Millisecond * 5 , wait .ForeverTestTimeout , func () (bool , error ) {
739
+ if err := wait .PollUntilContextTimeout ( context . Background (), time .Millisecond * 5 , wait .ForeverTestTimeout , true , func (_ context. Context ) (bool , error ) {
451
740
_ , err = client .CoreV1 ().Pods (markerNs ).Patch (context .TODO (), marker .Name , types .JSONPatchType , []byte ("[]" ), metav1.PatchOptions {})
452
741
select {
453
742
case <- upCh :
@@ -876,7 +1165,7 @@ func TestMatchConditionsWithStrictCostEnforcement(t *testing.T) {
876
1165
}()
877
1166
878
1167
// wait until new webhook is called the first time
879
- if err := wait .PollImmediate ( time .Millisecond * 5 , wait .ForeverTestTimeout , func () (bool , error ) {
1168
+ if err := wait .PollUntilContextTimeout ( context . Background (), time .Millisecond * 5 , wait .ForeverTestTimeout , true , func (_ context. Context ) (bool , error ) {
880
1169
_ , err = client .CoreV1 ().Pods (markerNs ).Patch (context .TODO (), marker .Name , types .JSONPatchType , []byte ("[]" ), metav1.PatchOptions {})
881
1170
select {
882
1171
case <- upCh :
0 commit comments