k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/validating/plugin_test.go (about)

     1  /*
     2  Copyright 2017 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 validating
    18  
    19  import (
    20  	"context"
    21  	"net/url"
    22  	"os"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/stretchr/testify/assert"
    27  	"k8s.io/apiserver/pkg/endpoints/request"
    28  	clocktesting "k8s.io/utils/clock/testing"
    29  
    30  	"k8s.io/apimachinery/pkg/api/errors"
    31  	webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
    32  	auditinternal "k8s.io/apiserver/pkg/apis/audit"
    33  )
    34  
    35  // BenchmarkValidate tests that ValidatingWebhook#Validate works as expected
    36  func BenchmarkValidate(b *testing.B) {
    37  	testServerURL := os.Getenv("WEBHOOK_TEST_SERVER_URL")
    38  	if len(testServerURL) == 0 {
    39  		b.Log("warning, WEBHOOK_TEST_SERVER_URL not set, starting in-process server, benchmarks will include webhook cost.")
    40  		b.Log("to run a standalone server, run:")
    41  		b.Log("go run k8s.io/apiserver/pkg/admission/plugin/webhook/testing/main/main.go")
    42  		testServer := webhooktesting.NewTestServer(b)
    43  		testServer.StartTLS()
    44  		defer testServer.Close()
    45  		testServerURL = testServer.URL
    46  	}
    47  
    48  	objectInterfaces := webhooktesting.NewObjectInterfacesForTest()
    49  
    50  	serverURL, err := url.ParseRequestURI(testServerURL)
    51  	if err != nil {
    52  		b.Fatalf("this should never happen? %v", err)
    53  	}
    54  
    55  	stopCh := make(chan struct{})
    56  	defer close(stopCh)
    57  
    58  	for _, tt := range webhooktesting.NewNonMutatingTestCases(serverURL) {
    59  		// For now, skip failure cases or tests that explicitly skip benchmarking
    60  		if !tt.ExpectAllow || tt.SkipBenchmark {
    61  			continue
    62  		}
    63  
    64  		b.Run(tt.Name, func(b *testing.B) {
    65  			wh, err := NewValidatingAdmissionWebhook(nil)
    66  			if err != nil {
    67  				b.Errorf("%s: failed to create validating webhook: %v", tt.Name, err)
    68  				return
    69  			}
    70  
    71  			ns := "webhook-test"
    72  			client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
    73  
    74  			wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
    75  			wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
    76  			wh.SetExternalKubeClientSet(client)
    77  			wh.SetExternalKubeInformerFactory(informer)
    78  
    79  			informer.Start(stopCh)
    80  			informer.WaitForCacheSync(stopCh)
    81  
    82  			if err = wh.ValidateInitialization(); err != nil {
    83  				b.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
    84  				return
    85  			}
    86  
    87  			attr := webhooktesting.NewAttribute(ns, nil, tt.IsDryRun)
    88  
    89  			b.ResetTimer()
    90  			b.RunParallel(func(pb *testing.PB) {
    91  				for pb.Next() {
    92  					wh.Validate(context.TODO(), attr, objectInterfaces)
    93  				}
    94  			})
    95  		})
    96  	}
    97  }
    98  
    99  // TestValidate tests that ValidatingWebhook#Validate works as expected
   100  func TestValidate(t *testing.T) {
   101  	testServer := webhooktesting.NewTestServer(t)
   102  	testServer.StartTLS()
   103  	defer testServer.Close()
   104  
   105  	objectInterfaces := webhooktesting.NewObjectInterfacesForTest()
   106  
   107  	serverURL, err := url.ParseRequestURI(testServer.URL)
   108  	if err != nil {
   109  		t.Fatalf("this should never happen? %v", err)
   110  	}
   111  
   112  	stopCh := make(chan struct{})
   113  	defer close(stopCh)
   114  
   115  	for _, tt := range webhooktesting.NewNonMutatingTestCases(serverURL) {
   116  		wh, err := NewValidatingAdmissionWebhook(nil)
   117  		if err != nil {
   118  			t.Errorf("%s: failed to create validating webhook: %v", tt.Name, err)
   119  			continue
   120  		}
   121  
   122  		ns := "webhook-test"
   123  		client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
   124  
   125  		wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
   126  		wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
   127  		wh.SetExternalKubeClientSet(client)
   128  		wh.SetExternalKubeInformerFactory(informer)
   129  
   130  		informer.Start(stopCh)
   131  		informer.WaitForCacheSync(stopCh)
   132  
   133  		if err = wh.ValidateInitialization(); err != nil {
   134  			t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
   135  			continue
   136  		}
   137  
   138  		attr := webhooktesting.NewAttribute(ns, nil, tt.IsDryRun)
   139  		err = wh.Validate(context.TODO(), attr, objectInterfaces)
   140  		if tt.ExpectAllow != (err == nil) {
   141  			t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
   142  		}
   143  		// ErrWebhookRejected is not an error for our purposes
   144  		if tt.ErrorContains != "" {
   145  			if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
   146  				t.Errorf("%s: expected an error saying %q, but got %v", tt.Name, tt.ErrorContains, err)
   147  			}
   148  		}
   149  		if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
   150  			t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
   151  		}
   152  		fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
   153  		if !ok {
   154  			t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
   155  			continue
   156  		}
   157  		if len(tt.ExpectAnnotations) == 0 {
   158  			assert.Empty(t, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
   159  		} else {
   160  			assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
   161  		}
   162  	}
   163  }
   164  
   165  // TestValidateCachedClient tests that ValidatingWebhook#Validate should cache restClient
   166  func TestValidateCachedClient(t *testing.T) {
   167  	testServer := webhooktesting.NewTestServer(t)
   168  	testServer.StartTLS()
   169  	defer testServer.Close()
   170  	serverURL, err := url.ParseRequestURI(testServer.URL)
   171  	if err != nil {
   172  		t.Fatalf("this should never happen? %v", err)
   173  	}
   174  
   175  	objectInterfaces := webhooktesting.NewObjectInterfacesForTest()
   176  
   177  	stopCh := make(chan struct{})
   178  	defer close(stopCh)
   179  
   180  	wh, err := NewValidatingAdmissionWebhook(nil)
   181  	if err != nil {
   182  		t.Fatalf("Failed to create validating webhook: %v", err)
   183  	}
   184  	wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
   185  
   186  	for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
   187  		ns := "webhook-test"
   188  		client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
   189  
   190  		// override the webhook source. The client cache will stay the same.
   191  		cacheMisses := new(int32)
   192  		wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(cacheMisses)))
   193  		wh.SetExternalKubeClientSet(client)
   194  		wh.SetExternalKubeInformerFactory(informer)
   195  
   196  		informer.Start(stopCh)
   197  		informer.WaitForCacheSync(stopCh)
   198  
   199  		if err = wh.ValidateInitialization(); err != nil {
   200  			t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
   201  			continue
   202  		}
   203  
   204  		err = wh.Validate(context.TODO(), webhooktesting.NewAttribute(ns, nil, false), objectInterfaces)
   205  		if tt.ExpectAllow != (err == nil) {
   206  			t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
   207  		}
   208  
   209  		if tt.ExpectCacheMiss && *cacheMisses == 0 {
   210  			t.Errorf("%s: expected cache miss, but got no AuthenticationInfoResolver call", tt.Name)
   211  		}
   212  
   213  		if !tt.ExpectCacheMiss && *cacheMisses > 0 {
   214  			t.Errorf("%s: expected client to be cached, but got %d AuthenticationInfoResolver calls", tt.Name, *cacheMisses)
   215  		}
   216  	}
   217  }
   218  
   219  // TestValidateWebhookDuration tests that ValidatingWebhook#Validate sets webhook duration in context correctly
   220  func TestValidateWebhookDuration(ts *testing.T) {
   221  	clk := clocktesting.FakeClock{}
   222  	testServer := webhooktesting.NewTestServerWithHandler(ts, webhooktesting.ClockSteppingWebhookHandler(ts, &clk))
   223  	testServer.StartTLS()
   224  	defer testServer.Close()
   225  	serverURL, err := url.ParseRequestURI(testServer.URL)
   226  	if err != nil {
   227  		ts.Fatalf("this should never happen? %v", err)
   228  	}
   229  
   230  	objectInterfaces := webhooktesting.NewObjectInterfacesForTest()
   231  
   232  	stopCh := make(chan struct{})
   233  	defer close(stopCh)
   234  
   235  	for _, test := range webhooktesting.NewValidationDurationTestCases(serverURL) {
   236  		ts.Run(test.Name, func(t *testing.T) {
   237  			ctx := context.TODO()
   238  			if test.InitContext {
   239  				ctx = request.WithLatencyTrackersAndCustomClock(ctx, &clk)
   240  			}
   241  			wh, err := NewValidatingAdmissionWebhook(nil)
   242  			if err != nil {
   243  				t.Errorf("failed to create mutating webhook: %v", err)
   244  				return
   245  			}
   246  
   247  			ns := "webhook-test"
   248  			client, informer := webhooktesting.NewFakeValidatingDataSource(ns, test.Webhooks, stopCh)
   249  
   250  			wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
   251  			wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
   252  			wh.SetExternalKubeClientSet(client)
   253  			wh.SetExternalKubeInformerFactory(informer)
   254  
   255  			informer.Start(stopCh)
   256  			informer.WaitForCacheSync(stopCh)
   257  
   258  			if err = wh.ValidateInitialization(); err != nil {
   259  				t.Errorf("failed to validate initialization: %v", err)
   260  				return
   261  			}
   262  
   263  			_ = wh.Validate(ctx, webhooktesting.NewAttribute(ns, nil, test.IsDryRun), objectInterfaces)
   264  			wd, ok := request.LatencyTrackersFrom(ctx)
   265  			if !ok {
   266  				if test.InitContext {
   267  					t.Errorf("expected webhook duration to be initialized")
   268  				}
   269  				return
   270  			}
   271  			if !test.InitContext {
   272  				t.Errorf("expected webhook duration to not be initialized")
   273  				return
   274  			}
   275  			if wd.MutatingWebhookTracker.GetLatency() != 0 {
   276  				t.Errorf("expected admit duration to be equal to 0 got %q", wd.MutatingWebhookTracker.GetLatency())
   277  			}
   278  			if wd.ValidatingWebhookTracker.GetLatency() < test.ExpectedDurationMax {
   279  				t.Errorf("expected validate duraion to be greater or equal to %q got %q", test.ExpectedDurationMax, wd.ValidatingWebhookTracker.GetLatency())
   280  			}
   281  		})
   282  	}
   283  }
   284  
   285  // TestValidatePanicHandling tests that panics should not escape the dispatcher
   286  func TestValidatePanicHandling(t *testing.T) {
   287  	testServer := webhooktesting.NewTestServer(t)
   288  	testServer.StartTLS()
   289  	defer testServer.Close()
   290  
   291  	objectInterfaces := webhooktesting.NewObjectInterfacesForTest()
   292  
   293  	serverURL, err := url.ParseRequestURI(testServer.URL)
   294  	if err != nil {
   295  		t.Fatalf("this should never happen? %v", err)
   296  	}
   297  
   298  	stopCh := make(chan struct{})
   299  	defer close(stopCh)
   300  
   301  	for _, tt := range webhooktesting.NewNonMutatingPanicTestCases(serverURL) {
   302  		wh, err := NewValidatingAdmissionWebhook(nil)
   303  		if err != nil {
   304  			t.Errorf("%s: failed to create validating webhook: %v", tt.Name, err)
   305  			continue
   306  		}
   307  
   308  		ns := "webhook-test"
   309  		client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
   310  
   311  		wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewPanickingAuthenticationInfoResolver("Start panicking!"))) // see Aladdin, it's awesome
   312  		wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
   313  		wh.SetExternalKubeClientSet(client)
   314  		wh.SetExternalKubeInformerFactory(informer)
   315  
   316  		informer.Start(stopCh)
   317  		informer.WaitForCacheSync(stopCh)
   318  
   319  		if err = wh.ValidateInitialization(); err != nil {
   320  			t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
   321  			continue
   322  		}
   323  
   324  		attr := webhooktesting.NewAttribute(ns, nil, tt.IsDryRun)
   325  		err = wh.Validate(context.TODO(), attr, objectInterfaces)
   326  		if tt.ExpectAllow != (err == nil) {
   327  			t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
   328  		}
   329  		// ErrWebhookRejected is not an error for our purposes
   330  		if tt.ErrorContains != "" {
   331  			if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
   332  				t.Errorf("%s: expected an error saying %q, but got %v", tt.Name, tt.ErrorContains, err)
   333  			}
   334  		}
   335  		if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
   336  			t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
   337  		}
   338  		fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
   339  		if !ok {
   340  			t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
   341  			continue
   342  		}
   343  		if len(tt.ExpectAnnotations) == 0 {
   344  			assert.Empty(t, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
   345  		} else {
   346  			assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(auditinternal.LevelMetadata), tt.Name+": annotations not set as expected.")
   347  		}
   348  	}
   349  }