k8s.io/apiserver@v0.31.1/pkg/admission/configuration/validating_webhook_manager_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 configuration
    18  
    19  import (
    20  	"context"
    21  	"reflect"
    22  	"testing"
    23  	"time"
    24  
    25  	"k8s.io/api/admissionregistration/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apiserver/pkg/admission/plugin/webhook"
    28  	"k8s.io/client-go/informers"
    29  	"k8s.io/client-go/kubernetes/fake"
    30  )
    31  
    32  func TestGetValidatingWebhookConfig(t *testing.T) {
    33  	// Build a test client that the admission plugin can use to look up the ValidatingWebhookConfiguration
    34  	client := fake.NewSimpleClientset()
    35  	informerFactory := informers.NewSharedInformerFactory(client, 0)
    36  	stop := make(chan struct{})
    37  	defer close(stop)
    38  
    39  	manager := NewValidatingWebhookConfigurationManager(informerFactory)
    40  	informerFactory.Start(stop)
    41  	informerFactory.WaitForCacheSync(stop)
    42  
    43  	// no configurations
    44  	if configurations := manager.Webhooks(); len(configurations) != 0 {
    45  		t.Errorf("expected empty webhooks, but got %v", configurations)
    46  	}
    47  
    48  	webhookConfiguration := &v1.ValidatingWebhookConfiguration{
    49  		ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
    50  		Webhooks:   []v1.ValidatingWebhook{{Name: "webhook1.1"}},
    51  	}
    52  
    53  	client.
    54  		AdmissionregistrationV1().
    55  		ValidatingWebhookConfigurations().
    56  		Create(context.TODO(), webhookConfiguration, metav1.CreateOptions{})
    57  
    58  	// Wait up to 10s for the notification to be delivered.
    59  	// (on my system this takes < 2ms)
    60  	startTime := time.Now()
    61  	configurations := manager.Webhooks()
    62  	for len(configurations) == 0 {
    63  		if time.Since(startTime) > 10*time.Second {
    64  			break
    65  		}
    66  		time.Sleep(time.Millisecond)
    67  		configurations = manager.Webhooks()
    68  	}
    69  
    70  	// verify presence
    71  	if len(configurations) == 0 {
    72  		t.Errorf("expected non empty webhooks")
    73  	}
    74  	for i := range configurations {
    75  		h, ok := configurations[i].GetValidatingWebhook()
    76  		if !ok {
    77  			t.Errorf("Expected validating webhook")
    78  			continue
    79  		}
    80  		if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
    81  			t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
    82  		}
    83  	}
    84  }
    85  
    86  // mockCreateValidatingWebhookAccessor is a struct used to compute how many times
    87  // the function webhook.NewValidatingWebhookAccessor is being called when refreshing
    88  // webhookAccessors.
    89  //
    90  // NOTE: Maybe there some testing help that we can import and reuse instead.
    91  type mockCreateValidatingWebhookAccessor struct {
    92  	numberOfCalls int
    93  }
    94  
    95  func (mock *mockCreateValidatingWebhookAccessor) calledNTimes() int { return mock.numberOfCalls }
    96  func (mock *mockCreateValidatingWebhookAccessor) resetCounter()     { mock.numberOfCalls = 0 }
    97  func (mock *mockCreateValidatingWebhookAccessor) incrementCounter() { mock.numberOfCalls++ }
    98  
    99  func (mock *mockCreateValidatingWebhookAccessor) fn(uid string, configurationName string, h *v1.ValidatingWebhook) webhook.WebhookAccessor {
   100  	mock.incrementCounter()
   101  	return webhook.NewValidatingWebhookAccessor(uid, configurationName, h)
   102  }
   103  
   104  func configurationTotalWebhooks(configurations []*v1.ValidatingWebhookConfiguration) int {
   105  	total := 0
   106  	for _, configuration := range configurations {
   107  		total += len(configuration.Webhooks)
   108  	}
   109  	return total
   110  }
   111  
   112  func TestGetValidatingWebhookConfigSmartReload(t *testing.T) {
   113  	type args struct {
   114  		createWebhookConfigurations []*v1.ValidatingWebhookConfiguration
   115  		updateWebhookConfigurations []*v1.ValidatingWebhookConfiguration
   116  	}
   117  	tests := []struct {
   118  		name              string
   119  		args              args
   120  		numberOfCreations int
   121  		// number of refreshes are number of times we recrated a webhook accessor
   122  		// instead of pulling from the cache.
   123  		numberOfRefreshes             int
   124  		finalNumberOfWebhookAccessors int
   125  	}{
   126  		{
   127  			name: "no creations and no updates",
   128  			args: args{
   129  				nil,
   130  				nil,
   131  			},
   132  			numberOfCreations:             0,
   133  			numberOfRefreshes:             0,
   134  			finalNumberOfWebhookAccessors: 0,
   135  		},
   136  		{
   137  			name: "create configurations and no updates",
   138  			args: args{
   139  				[]*v1.ValidatingWebhookConfiguration{
   140  					{
   141  						ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
   142  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook1.1"}},
   143  					},
   144  					{
   145  						ObjectMeta: metav1.ObjectMeta{Name: "webhook2"},
   146  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook2.1"}},
   147  					},
   148  				},
   149  				nil,
   150  			},
   151  			numberOfCreations:             2,
   152  			numberOfRefreshes:             0,
   153  			finalNumberOfWebhookAccessors: 2,
   154  		},
   155  		{
   156  			name: "create configurations and update some of them",
   157  			args: args{
   158  				[]*v1.ValidatingWebhookConfiguration{
   159  					{
   160  						ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
   161  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook3.1"}},
   162  					},
   163  					{
   164  						ObjectMeta: metav1.ObjectMeta{Name: "webhook4"},
   165  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook4.1"}},
   166  					},
   167  				},
   168  				[]*v1.ValidatingWebhookConfiguration{
   169  					{
   170  						ObjectMeta: metav1.ObjectMeta{Name: "webhook3"},
   171  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook3.1-updated"}},
   172  					},
   173  				},
   174  			},
   175  			numberOfCreations:             2,
   176  			numberOfRefreshes:             1,
   177  			finalNumberOfWebhookAccessors: 2,
   178  		},
   179  		{
   180  			name: "create configuration and update moar of them",
   181  			args: args{
   182  				[]*v1.ValidatingWebhookConfiguration{
   183  					{
   184  						ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
   185  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook5.1"}, {Name: "webhook5.2"}},
   186  					},
   187  					{
   188  						ObjectMeta: metav1.ObjectMeta{Name: "webhook6"},
   189  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook6.1"}},
   190  					},
   191  					{
   192  						ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
   193  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook7.1"}, {Name: "webhook7.1"}},
   194  					},
   195  				},
   196  				[]*v1.ValidatingWebhookConfiguration{
   197  					{
   198  						ObjectMeta: metav1.ObjectMeta{Name: "webhook5"},
   199  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook5.1-updated"}},
   200  					},
   201  					{
   202  						ObjectMeta: metav1.ObjectMeta{Name: "webhook7"},
   203  						Webhooks:   []v1.ValidatingWebhook{{Name: "webhook7.1-updated"}, {Name: "webhook7.2-updated"}, {Name: "webhook7.3"}},
   204  					},
   205  				},
   206  			},
   207  			numberOfCreations:             5,
   208  			numberOfRefreshes:             4,
   209  			finalNumberOfWebhookAccessors: 5,
   210  		},
   211  	}
   212  
   213  	for _, tt := range tests {
   214  		t.Run(tt.name, func(t *testing.T) {
   215  			client := fake.NewSimpleClientset()
   216  			informerFactory := informers.NewSharedInformerFactory(client, 0)
   217  			stop := make(chan struct{})
   218  			defer close(stop)
   219  			manager := NewValidatingWebhookConfigurationManager(informerFactory)
   220  			managerStructPtr := manager.(*validatingWebhookConfigurationManager)
   221  			fakeWebhookAccessorCreator := &mockCreateValidatingWebhookAccessor{}
   222  			managerStructPtr.createValidatingWebhookAccessor = fakeWebhookAccessorCreator.fn
   223  			informerFactory.Start(stop)
   224  			informerFactory.WaitForCacheSync(stop)
   225  
   226  			// Create webhooks
   227  			for _, configurations := range tt.args.createWebhookConfigurations {
   228  				client.
   229  					AdmissionregistrationV1().
   230  					ValidatingWebhookConfigurations().
   231  					Create(context.TODO(), configurations, metav1.CreateOptions{})
   232  			}
   233  			// TODO use channels to wait for manager.createValidatingWebhookAccessor
   234  			// to be called instead of using time.Sleep
   235  			time.Sleep(1 * time.Second)
   236  			webhooks := manager.Webhooks()
   237  			if configurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) {
   238  				t.Errorf("Expected number of webhooks %d received %d",
   239  					configurationTotalWebhooks(tt.args.createWebhookConfigurations),
   240  					len(webhooks),
   241  				)
   242  			}
   243  			// assert creations
   244  			if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() {
   245  				t.Errorf(
   246  					"Expected number of creations %d received %d",
   247  					tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(),
   248  				)
   249  			}
   250  
   251  			// reset mock counter
   252  			fakeWebhookAccessorCreator.resetCounter()
   253  
   254  			// Update webhooks
   255  			for _, configurations := range tt.args.updateWebhookConfigurations {
   256  				client.
   257  					AdmissionregistrationV1().
   258  					ValidatingWebhookConfigurations().
   259  					Update(context.TODO(), configurations, metav1.UpdateOptions{})
   260  			}
   261  			// TODO use channels to wait for manager.createValidatingWebhookAccessor
   262  			// to be called instead of using time.Sleep
   263  			time.Sleep(1 * time.Second)
   264  			webhooks = manager.Webhooks()
   265  			if tt.finalNumberOfWebhookAccessors != len(webhooks) {
   266  				t.Errorf("Expected final number of webhooks %d received %d",
   267  					tt.finalNumberOfWebhookAccessors,
   268  					len(webhooks),
   269  				)
   270  			}
   271  
   272  			// assert updates
   273  			if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() {
   274  				t.Errorf(
   275  					"Expected number of refreshes %d received %d",
   276  					tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(),
   277  				)
   278  			}
   279  			// reset mock counter for the next test cases
   280  			fakeWebhookAccessorCreator.resetCounter()
   281  		})
   282  	}
   283  }