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 }