sigs.k8s.io/controller-runtime@v0.18.2/pkg/builder/webhook_test.go (about) 1 /* 2 Copyright 2019 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 builder 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "os" 27 "strings" 28 29 "github.com/go-logr/logr" 30 . "github.com/onsi/ginkgo/v2" 31 . "github.com/onsi/gomega" 32 "github.com/onsi/gomega/gbytes" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 37 "sigs.k8s.io/controller-runtime/pkg/controller" 38 logf "sigs.k8s.io/controller-runtime/pkg/log" 39 "sigs.k8s.io/controller-runtime/pkg/log/zap" 40 "sigs.k8s.io/controller-runtime/pkg/manager" 41 "sigs.k8s.io/controller-runtime/pkg/scheme" 42 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 43 ) 44 45 const ( 46 admissionReviewGV = `{ 47 "kind":"AdmissionReview", 48 "apiVersion":"admission.k8s.io/` 49 50 svcBaseAddr = "http://svc-name.svc-ns.svc" 51 ) 52 53 var _ = Describe("webhook", func() { 54 Describe("New", func() { 55 Context("v1 AdmissionReview", func() { 56 runTests("v1") 57 }) 58 Context("v1beta1 AdmissionReview", func() { 59 runTests("v1beta1") 60 }) 61 }) 62 }) 63 64 func runTests(admissionReviewVersion string) { 65 var ( 66 stop chan struct{} 67 logBuffer *gbytes.Buffer 68 testingLogger logr.Logger 69 ) 70 71 BeforeEach(func() { 72 stop = make(chan struct{}) 73 newController = controller.New 74 logBuffer = gbytes.NewBuffer() 75 testingLogger = zap.New(zap.JSONEncoder(), zap.WriteTo(io.MultiWriter(logBuffer, GinkgoWriter))) 76 }) 77 78 AfterEach(func() { 79 close(stop) 80 }) 81 82 It("should scaffold a defaulting webhook if the type implements the Defaulter interface", func() { 83 By("creating a controller manager") 84 m, err := manager.New(cfg, manager.Options{}) 85 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 86 87 By("registering the type in the Scheme") 88 builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} 89 builder.Register(&TestDefaulter{}, &TestDefaulterList{}) 90 err = builder.AddToScheme(m.GetScheme()) 91 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 92 93 err = WebhookManagedBy(m). 94 For(&TestDefaulter{}). 95 Complete() 96 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 97 svr := m.GetWebhookServer() 98 ExpectWithOffset(1, svr).NotTo(BeNil()) 99 100 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 101 "request":{ 102 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 103 "kind":{ 104 "group":"", 105 "version":"v1", 106 "kind":"TestDefaulter" 107 }, 108 "resource":{ 109 "group":"", 110 "version":"v1", 111 "resource":"testdefaulter" 112 }, 113 "namespace":"default", 114 "operation":"CREATE", 115 "object":{ 116 "replica":1 117 }, 118 "oldObject":null 119 } 120 }`) 121 122 ctx, cancel := context.WithCancel(context.Background()) 123 cancel() 124 err = svr.Start(ctx) 125 if err != nil && !os.IsNotExist(err) { 126 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 127 } 128 129 By("sending a request to a mutating webhook path") 130 path := generateMutatePath(testDefaulterGVK) 131 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 132 req.Header.Add("Content-Type", "application/json") 133 w := httptest.NewRecorder() 134 svr.WebhookMux().ServeHTTP(w, req) 135 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 136 By("sanity checking the response contains reasonable fields") 137 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) 138 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) 139 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) 140 141 By("sending a request to a validating webhook path that doesn't exist") 142 path = generateValidatePath(testDefaulterGVK) 143 _, err = reader.Seek(0, 0) 144 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 145 req = httptest.NewRequest("POST", svcBaseAddr+path, reader) 146 req.Header.Add("Content-Type", "application/json") 147 w = httptest.NewRecorder() 148 svr.WebhookMux().ServeHTTP(w, req) 149 ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) 150 }) 151 152 It("should scaffold a defaulting webhook which recovers from panics", func() { 153 By("creating a controller manager") 154 m, err := manager.New(cfg, manager.Options{}) 155 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 156 157 By("registering the type in the Scheme") 158 builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} 159 builder.Register(&TestDefaulter{}, &TestDefaulterList{}) 160 err = builder.AddToScheme(m.GetScheme()) 161 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 162 163 err = WebhookManagedBy(m). 164 For(&TestDefaulter{Panic: true}). 165 RecoverPanic(). 166 Complete() 167 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 168 svr := m.GetWebhookServer() 169 ExpectWithOffset(1, svr).NotTo(BeNil()) 170 171 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 172 "request":{ 173 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 174 "kind":{ 175 "group":"", 176 "version":"v1", 177 "kind":"TestDefaulter" 178 }, 179 "resource":{ 180 "group":"", 181 "version":"v1", 182 "resource":"testdefaulter" 183 }, 184 "namespace":"default", 185 "operation":"CREATE", 186 "object":{ 187 "replica":1, 188 "panic":true 189 }, 190 "oldObject":null 191 } 192 }`) 193 194 ctx, cancel := context.WithCancel(context.Background()) 195 cancel() 196 err = svr.Start(ctx) 197 if err != nil && !os.IsNotExist(err) { 198 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 199 } 200 201 By("sending a request to a mutating webhook path") 202 path := generateMutatePath(testDefaulterGVK) 203 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 204 req.Header.Add("Content-Type", "application/json") 205 w := httptest.NewRecorder() 206 svr.WebhookMux().ServeHTTP(w, req) 207 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 208 By("sanity checking the response contains reasonable fields") 209 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) 210 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) 211 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) 212 }) 213 214 It("should scaffold a defaulting webhook with a custom defaulter", func() { 215 By("creating a controller manager") 216 m, err := manager.New(cfg, manager.Options{}) 217 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 218 219 By("registering the type in the Scheme") 220 builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} 221 builder.Register(&TestDefaulter{}, &TestDefaulterList{}) 222 err = builder.AddToScheme(m.GetScheme()) 223 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 224 225 err = WebhookManagedBy(m). 226 WithDefaulter(&TestCustomDefaulter{}). 227 For(&TestDefaulter{}). 228 WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { 229 return admission.DefaultLogConstructor(testingLogger, req) 230 }). 231 Complete() 232 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 233 svr := m.GetWebhookServer() 234 ExpectWithOffset(1, svr).NotTo(BeNil()) 235 236 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 237 "request":{ 238 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 239 "kind":{ 240 "group":"foo.test.org", 241 "version":"v1", 242 "kind":"TestDefaulter" 243 }, 244 "resource":{ 245 "group":"foo.test.org", 246 "version":"v1", 247 "resource":"testdefaulter" 248 }, 249 "namespace":"default", 250 "name":"foo", 251 "operation":"CREATE", 252 "object":{ 253 "replica":1 254 }, 255 "oldObject":null 256 } 257 }`) 258 259 ctx, cancel := context.WithCancel(context.Background()) 260 cancel() 261 err = svr.Start(ctx) 262 if err != nil && !os.IsNotExist(err) { 263 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 264 } 265 266 By("sending a request to a mutating webhook path") 267 path := generateMutatePath(testDefaulterGVK) 268 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 269 req.Header.Add("Content-Type", "application/json") 270 w := httptest.NewRecorder() 271 svr.WebhookMux().ServeHTTP(w, req) 272 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 273 By("sanity checking the response contains reasonable fields") 274 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) 275 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) 276 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) 277 EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) 278 279 By("sending a request to a validating webhook path that doesn't exist") 280 path = generateValidatePath(testDefaulterGVK) 281 _, err = reader.Seek(0, 0) 282 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 283 req = httptest.NewRequest("POST", svcBaseAddr+path, reader) 284 req.Header.Add("Content-Type", "application/json") 285 w = httptest.NewRecorder() 286 svr.WebhookMux().ServeHTTP(w, req) 287 ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) 288 }) 289 290 It("should scaffold a validating webhook if the type implements the Validator interface", func() { 291 By("creating a controller manager") 292 m, err := manager.New(cfg, manager.Options{}) 293 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 294 295 By("registering the type in the Scheme") 296 builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} 297 builder.Register(&TestValidator{}, &TestValidatorList{}) 298 err = builder.AddToScheme(m.GetScheme()) 299 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 300 301 err = WebhookManagedBy(m). 302 For(&TestValidator{}). 303 Complete() 304 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 305 svr := m.GetWebhookServer() 306 ExpectWithOffset(1, svr).NotTo(BeNil()) 307 308 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 309 "request":{ 310 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 311 "kind":{ 312 "group":"", 313 "version":"v1", 314 "kind":"TestValidator" 315 }, 316 "resource":{ 317 "group":"", 318 "version":"v1", 319 "resource":"testvalidator" 320 }, 321 "namespace":"default", 322 "operation":"UPDATE", 323 "object":{ 324 "replica":1 325 }, 326 "oldObject":{ 327 "replica":2 328 } 329 } 330 }`) 331 332 ctx, cancel := context.WithCancel(context.Background()) 333 cancel() 334 err = svr.Start(ctx) 335 if err != nil && !os.IsNotExist(err) { 336 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 337 } 338 339 By("sending a request to a mutating webhook path that doesn't exist") 340 path := generateMutatePath(testValidatorGVK) 341 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 342 req.Header.Add("Content-Type", "application/json") 343 w := httptest.NewRecorder() 344 svr.WebhookMux().ServeHTTP(w, req) 345 ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) 346 347 By("sending a request to a validating webhook path") 348 path = generateValidatePath(testValidatorGVK) 349 _, err = reader.Seek(0, 0) 350 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 351 req = httptest.NewRequest("POST", svcBaseAddr+path, reader) 352 req.Header.Add("Content-Type", "application/json") 353 w = httptest.NewRecorder() 354 svr.WebhookMux().ServeHTTP(w, req) 355 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 356 By("sanity checking the response contains reasonable field") 357 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) 358 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) 359 }) 360 361 It("should scaffold a validating webhook which recovers from panics", func() { 362 By("creating a controller manager") 363 m, err := manager.New(cfg, manager.Options{}) 364 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 365 366 By("registering the type in the Scheme") 367 builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} 368 builder.Register(&TestValidator{}, &TestValidatorList{}) 369 err = builder.AddToScheme(m.GetScheme()) 370 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 371 372 err = WebhookManagedBy(m). 373 For(&TestValidator{Panic: true}). 374 RecoverPanic(). 375 Complete() 376 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 377 svr := m.GetWebhookServer() 378 ExpectWithOffset(1, svr).NotTo(BeNil()) 379 380 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 381 "request":{ 382 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 383 "kind":{ 384 "group":"", 385 "version":"v1", 386 "kind":"TestValidator" 387 }, 388 "resource":{ 389 "group":"", 390 "version":"v1", 391 "resource":"testvalidator" 392 }, 393 "namespace":"default", 394 "operation":"CREATE", 395 "object":{ 396 "replica":2, 397 "panic":true 398 } 399 } 400 }`) 401 402 ctx, cancel := context.WithCancel(context.Background()) 403 cancel() 404 err = svr.Start(ctx) 405 if err != nil && !os.IsNotExist(err) { 406 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 407 } 408 409 By("sending a request to a validating webhook path") 410 path := generateValidatePath(testValidatorGVK) 411 _, err = reader.Seek(0, 0) 412 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 413 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 414 req.Header.Add("Content-Type", "application/json") 415 w := httptest.NewRecorder() 416 svr.WebhookMux().ServeHTTP(w, req) 417 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 418 By("sanity checking the response contains reasonable field") 419 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) 420 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) 421 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) 422 }) 423 424 It("should scaffold a validating webhook with a custom validator", func() { 425 By("creating a controller manager") 426 m, err := manager.New(cfg, manager.Options{}) 427 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 428 429 By("registering the type in the Scheme") 430 builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} 431 builder.Register(&TestValidator{}, &TestValidatorList{}) 432 err = builder.AddToScheme(m.GetScheme()) 433 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 434 435 err = WebhookManagedBy(m). 436 WithValidator(&TestCustomValidator{}). 437 For(&TestValidator{}). 438 WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { 439 return admission.DefaultLogConstructor(testingLogger, req) 440 }). 441 Complete() 442 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 443 svr := m.GetWebhookServer() 444 ExpectWithOffset(1, svr).NotTo(BeNil()) 445 446 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 447 "request":{ 448 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 449 "kind":{ 450 "group":"foo.test.org", 451 "version":"v1", 452 "kind":"TestValidator" 453 }, 454 "resource":{ 455 "group":"foo.test.org", 456 "version":"v1", 457 "resource":"testvalidator" 458 }, 459 "namespace":"default", 460 "name":"foo", 461 "operation":"UPDATE", 462 "object":{ 463 "replica":1 464 }, 465 "oldObject":{ 466 "replica":2 467 } 468 } 469 }`) 470 471 ctx, cancel := context.WithCancel(context.Background()) 472 cancel() 473 err = svr.Start(ctx) 474 if err != nil && !os.IsNotExist(err) { 475 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 476 } 477 478 By("sending a request to a mutating webhook path that doesn't exist") 479 path := generateMutatePath(testValidatorGVK) 480 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 481 req.Header.Add("Content-Type", "application/json") 482 w := httptest.NewRecorder() 483 svr.WebhookMux().ServeHTTP(w, req) 484 ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) 485 486 By("sending a request to a validating webhook path") 487 path = generateValidatePath(testValidatorGVK) 488 _, err = reader.Seek(0, 0) 489 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 490 req = httptest.NewRequest("POST", svcBaseAddr+path, reader) 491 req.Header.Add("Content-Type", "application/json") 492 w = httptest.NewRecorder() 493 svr.WebhookMux().ServeHTTP(w, req) 494 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 495 By("sanity checking the response contains reasonable field") 496 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) 497 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) 498 EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) 499 }) 500 501 It("should scaffold defaulting and validating webhooks if the type implements both Defaulter and Validator interfaces", func() { 502 By("creating a controller manager") 503 m, err := manager.New(cfg, manager.Options{}) 504 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 505 506 By("registering the type in the Scheme") 507 builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()} 508 builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) 509 err = builder.AddToScheme(m.GetScheme()) 510 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 511 512 err = WebhookManagedBy(m). 513 For(&TestDefaultValidator{}). 514 Complete() 515 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 516 svr := m.GetWebhookServer() 517 ExpectWithOffset(1, svr).NotTo(BeNil()) 518 519 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 520 "request":{ 521 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 522 "kind":{ 523 "group":"", 524 "version":"v1", 525 "kind":"TestDefaultValidator" 526 }, 527 "resource":{ 528 "group":"", 529 "version":"v1", 530 "resource":"testdefaultvalidator" 531 }, 532 "namespace":"default", 533 "operation":"CREATE", 534 "object":{ 535 "replica":1 536 }, 537 "oldObject":null 538 } 539 }`) 540 541 ctx, cancel := context.WithCancel(context.Background()) 542 cancel() 543 err = svr.Start(ctx) 544 if err != nil && !os.IsNotExist(err) { 545 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 546 } 547 548 By("sending a request to a mutating webhook path") 549 path := generateMutatePath(testDefaultValidatorGVK) 550 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 551 req.Header.Add("Content-Type", "application/json") 552 w := httptest.NewRecorder() 553 svr.WebhookMux().ServeHTTP(w, req) 554 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 555 By("sanity checking the response contains reasonable field") 556 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) 557 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) 558 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) 559 560 By("sending a request to a validating webhook path") 561 path = generateValidatePath(testDefaultValidatorGVK) 562 _, err = reader.Seek(0, 0) 563 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 564 req = httptest.NewRequest("POST", svcBaseAddr+path, reader) 565 req.Header.Add("Content-Type", "application/json") 566 w = httptest.NewRecorder() 567 svr.WebhookMux().ServeHTTP(w, req) 568 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 569 By("sanity checking the response contains reasonable field") 570 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) 571 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) 572 }) 573 574 It("should scaffold a validating webhook if the type implements the Validator interface to validate deletes", func() { 575 By("creating a controller manager") 576 ctx, cancel := context.WithCancel(context.Background()) 577 578 m, err := manager.New(cfg, manager.Options{}) 579 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 580 581 By("registering the type in the Scheme") 582 builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} 583 builder.Register(&TestValidator{}, &TestValidatorList{}) 584 err = builder.AddToScheme(m.GetScheme()) 585 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 586 587 err = WebhookManagedBy(m). 588 For(&TestValidator{}). 589 Complete() 590 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 591 svr := m.GetWebhookServer() 592 ExpectWithOffset(1, svr).NotTo(BeNil()) 593 594 reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 595 "request":{ 596 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 597 "kind":{ 598 "group":"", 599 "version":"v1", 600 "kind":"TestValidator" 601 }, 602 "resource":{ 603 "group":"", 604 "version":"v1", 605 "resource":"testvalidator" 606 }, 607 "namespace":"default", 608 "operation":"DELETE", 609 "object":null, 610 "oldObject":{ 611 "replica":1 612 } 613 } 614 }`) 615 616 cancel() 617 err = svr.Start(ctx) 618 if err != nil && !os.IsNotExist(err) { 619 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 620 } 621 622 By("sending a request to a validating webhook path to check for failed delete") 623 path := generateValidatePath(testValidatorGVK) 624 req := httptest.NewRequest("POST", svcBaseAddr+path, reader) 625 req.Header.Add("Content-Type", "application/json") 626 w := httptest.NewRecorder() 627 svr.WebhookMux().ServeHTTP(w, req) 628 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 629 By("sanity checking the response contains reasonable field") 630 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) 631 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) 632 633 reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `", 634 "request":{ 635 "uid":"07e52e8d-4513-11e9-a716-42010a800270", 636 "kind":{ 637 "group":"", 638 "version":"v1", 639 "kind":"TestValidator" 640 }, 641 "resource":{ 642 "group":"", 643 "version":"v1", 644 "resource":"testvalidator" 645 }, 646 "namespace":"default", 647 "operation":"DELETE", 648 "object":null, 649 "oldObject":{ 650 "replica":0 651 } 652 } 653 }`) 654 By("sending a request to a validating webhook path with correct request") 655 path = generateValidatePath(testValidatorGVK) 656 req = httptest.NewRequest("POST", svcBaseAddr+path, reader) 657 req.Header.Add("Content-Type", "application/json") 658 w = httptest.NewRecorder() 659 svr.WebhookMux().ServeHTTP(w, req) 660 ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) 661 By("sanity checking the response contains reasonable field") 662 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) 663 ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) 664 }) 665 666 It("should send an error when trying to register a webhook with more than one For", func() { 667 By("creating a controller manager") 668 m, err := manager.New(cfg, manager.Options{}) 669 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 670 671 By("registering the type in the Scheme") 672 builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} 673 builder.Register(&TestDefaulter{}, &TestDefaulterList{}) 674 err = builder.AddToScheme(m.GetScheme()) 675 ExpectWithOffset(1, err).NotTo(HaveOccurred()) 676 677 err = WebhookManagedBy(m). 678 For(&TestDefaulter{}). 679 For(&TestDefaulter{}). 680 Complete() 681 Expect(err).To(HaveOccurred()) 682 }) 683 } 684 685 // TestDefaulter. 686 var _ runtime.Object = &TestDefaulter{} 687 688 const testDefaulterKind = "TestDefaulter" 689 690 type TestDefaulter struct { 691 Replica int `json:"replica,omitempty"` 692 Panic bool `json:"panic,omitempty"` 693 } 694 695 var testDefaulterGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testDefaulterKind} 696 697 func (d *TestDefaulter) GetObjectKind() schema.ObjectKind { return d } 698 func (d *TestDefaulter) DeepCopyObject() runtime.Object { 699 return &TestDefaulter{ 700 Replica: d.Replica, 701 } 702 } 703 704 func (d *TestDefaulter) GroupVersionKind() schema.GroupVersionKind { 705 return testDefaulterGVK 706 } 707 708 func (d *TestDefaulter) SetGroupVersionKind(gvk schema.GroupVersionKind) {} 709 710 var _ runtime.Object = &TestDefaulterList{} 711 712 type TestDefaulterList struct{} 713 714 func (*TestDefaulterList) GetObjectKind() schema.ObjectKind { return nil } 715 func (*TestDefaulterList) DeepCopyObject() runtime.Object { return nil } 716 717 func (d *TestDefaulter) Default() { 718 if d.Panic { 719 panic("fake panic test") 720 } 721 if d.Replica < 2 { 722 d.Replica = 2 723 } 724 } 725 726 // TestValidator. 727 var _ runtime.Object = &TestValidator{} 728 729 const testValidatorKind = "TestValidator" 730 731 type TestValidator struct { 732 Replica int `json:"replica,omitempty"` 733 Panic bool `json:"panic,omitempty"` 734 } 735 736 var testValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testValidatorKind} 737 738 func (v *TestValidator) GetObjectKind() schema.ObjectKind { return v } 739 func (v *TestValidator) DeepCopyObject() runtime.Object { 740 return &TestValidator{ 741 Replica: v.Replica, 742 } 743 } 744 745 func (v *TestValidator) GroupVersionKind() schema.GroupVersionKind { 746 return testValidatorGVK 747 } 748 749 func (v *TestValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {} 750 751 var _ runtime.Object = &TestValidatorList{} 752 753 type TestValidatorList struct{} 754 755 func (*TestValidatorList) GetObjectKind() schema.ObjectKind { return nil } 756 func (*TestValidatorList) DeepCopyObject() runtime.Object { return nil } 757 758 var _ admission.Validator = &TestValidator{} 759 760 func (v *TestValidator) ValidateCreate() (admission.Warnings, error) { 761 if v.Panic { 762 panic("fake panic test") 763 } 764 if v.Replica < 0 { 765 return nil, errors.New("number of replica should be greater than or equal to 0") 766 } 767 return nil, nil 768 } 769 770 func (v *TestValidator) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 771 if v.Panic { 772 panic("fake panic test") 773 } 774 if v.Replica < 0 { 775 return nil, errors.New("number of replica should be greater than or equal to 0") 776 } 777 if oldObj, ok := old.(*TestValidator); !ok { 778 return nil, fmt.Errorf("the old object is expected to be %T", oldObj) 779 } else if v.Replica < oldObj.Replica { 780 return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, oldObj.Replica) 781 } 782 return nil, nil 783 } 784 785 func (v *TestValidator) ValidateDelete() (admission.Warnings, error) { 786 if v.Panic { 787 panic("fake panic test") 788 } 789 if v.Replica > 0 { 790 return nil, errors.New("number of replica should be less than or equal to 0 to delete") 791 } 792 return nil, nil 793 } 794 795 // TestDefaultValidator. 796 var _ runtime.Object = &TestDefaultValidator{} 797 798 type TestDefaultValidator struct { 799 metav1.TypeMeta 800 metav1.ObjectMeta 801 802 Replica int `json:"replica,omitempty"` 803 } 804 805 var testDefaultValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: "TestDefaultValidator"} 806 807 func (dv *TestDefaultValidator) GetObjectKind() schema.ObjectKind { return dv } 808 func (dv *TestDefaultValidator) DeepCopyObject() runtime.Object { 809 return &TestDefaultValidator{ 810 Replica: dv.Replica, 811 } 812 } 813 814 func (dv *TestDefaultValidator) GroupVersionKind() schema.GroupVersionKind { 815 return testDefaultValidatorGVK 816 } 817 818 func (dv *TestDefaultValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {} 819 820 var _ runtime.Object = &TestDefaultValidatorList{} 821 822 type TestDefaultValidatorList struct{} 823 824 func (*TestDefaultValidatorList) GetObjectKind() schema.ObjectKind { return nil } 825 func (*TestDefaultValidatorList) DeepCopyObject() runtime.Object { return nil } 826 827 func (dv *TestDefaultValidator) Default() { 828 if dv.Replica < 2 { 829 dv.Replica = 2 830 } 831 } 832 833 var _ admission.Validator = &TestDefaultValidator{} 834 835 func (dv *TestDefaultValidator) ValidateCreate() (admission.Warnings, error) { 836 if dv.Replica < 0 { 837 return nil, errors.New("number of replica should be greater than or equal to 0") 838 } 839 return nil, nil 840 } 841 842 func (dv *TestDefaultValidator) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 843 if dv.Replica < 0 { 844 return nil, errors.New("number of replica should be greater than or equal to 0") 845 } 846 return nil, nil 847 } 848 849 func (dv *TestDefaultValidator) ValidateDelete() (admission.Warnings, error) { 850 if dv.Replica > 0 { 851 return nil, errors.New("number of replica should be less than or equal to 0 to delete") 852 } 853 return nil, nil 854 } 855 856 // TestCustomDefaulter. 857 858 type TestCustomDefaulter struct{} 859 860 func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { 861 logf.FromContext(ctx).Info("Defaulting object") 862 req, err := admission.RequestFromContext(ctx) 863 if err != nil { 864 return fmt.Errorf("expected admission.Request in ctx: %w", err) 865 } 866 if req.Kind.Kind != testDefaulterKind { 867 return fmt.Errorf("expected Kind TestDefaulter got %q", req.Kind.Kind) 868 } 869 870 d := obj.(*TestDefaulter) //nolint:ifshort 871 if d.Replica < 2 { 872 d.Replica = 2 873 } 874 return nil 875 } 876 877 var _ admission.CustomDefaulter = &TestCustomDefaulter{} 878 879 // TestCustomValidator. 880 881 type TestCustomValidator struct{} 882 883 func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 884 logf.FromContext(ctx).Info("Validating object") 885 req, err := admission.RequestFromContext(ctx) 886 if err != nil { 887 return nil, fmt.Errorf("expected admission.Request in ctx: %w", err) 888 } 889 if req.Kind.Kind != testValidatorKind { 890 return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) 891 } 892 893 v := obj.(*TestValidator) //nolint:ifshort 894 if v.Replica < 0 { 895 return nil, errors.New("number of replica should be greater than or equal to 0") 896 } 897 return nil, nil 898 } 899 900 func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 901 logf.FromContext(ctx).Info("Validating object") 902 req, err := admission.RequestFromContext(ctx) 903 if err != nil { 904 return nil, fmt.Errorf("expected admission.Request in ctx: %w", err) 905 } 906 if req.Kind.Kind != testValidatorKind { 907 return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) 908 } 909 910 v := newObj.(*TestValidator) 911 old := oldObj.(*TestValidator) 912 if v.Replica < 0 { 913 return nil, errors.New("number of replica should be greater than or equal to 0") 914 } 915 if v.Replica < old.Replica { 916 return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, old.Replica) 917 } 918 return nil, nil 919 } 920 921 func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 922 logf.FromContext(ctx).Info("Validating object") 923 req, err := admission.RequestFromContext(ctx) 924 if err != nil { 925 return nil, fmt.Errorf("expected admission.Request in ctx: %w", err) 926 } 927 if req.Kind.Kind != testValidatorKind { 928 return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) 929 } 930 931 v := obj.(*TestValidator) //nolint:ifshort 932 if v.Replica > 0 { 933 return nil, errors.New("number of replica should be less than or equal to 0 to delete") 934 } 935 return nil, nil 936 } 937 938 var _ admission.CustomValidator = &TestCustomValidator{}