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 }