sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/admission/validator_test.go (about) 1 /* 2 Copyright 2021 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 admission 18 19 import ( 20 "context" 21 "errors" 22 "net/http" 23 24 . "github.com/onsi/ginkgo/v2" 25 . "github.com/onsi/gomega" 26 admissionv1 "k8s.io/api/admission/v1" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 "k8s.io/client-go/kubernetes/scheme" 32 ) 33 34 var fakeValidatorVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: "fakeValidator"} 35 36 var _ = Describe("validatingHandler", func() { 37 38 decoder := NewDecoder(scheme.Scheme) 39 40 Context("when dealing with successful results without warning", func() { 41 f := &fakeValidator{ErrorToReturn: nil, GVKToReturn: fakeValidatorVK, WarningsToReturn: nil} 42 handler := validatingHandler{validator: f, decoder: decoder} 43 44 It("should return 200 in response when create succeeds", func() { 45 46 response := handler.Handle(context.TODO(), Request{ 47 AdmissionRequest: admissionv1.AdmissionRequest{ 48 Operation: admissionv1.Create, 49 Object: runtime.RawExtension{ 50 Raw: []byte("{}"), 51 Object: handler.validator, 52 }, 53 }, 54 }) 55 56 Expect(response.Allowed).Should(BeTrue()) 57 Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) 58 }) 59 60 It("should return 200 in response when update succeeds", func() { 61 62 response := handler.Handle(context.TODO(), Request{ 63 AdmissionRequest: admissionv1.AdmissionRequest{ 64 Operation: admissionv1.Update, 65 Object: runtime.RawExtension{ 66 Raw: []byte("{}"), 67 Object: handler.validator, 68 }, 69 OldObject: runtime.RawExtension{ 70 Raw: []byte("{}"), 71 Object: handler.validator, 72 }, 73 }, 74 }) 75 Expect(response.Allowed).Should(BeTrue()) 76 Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) 77 }) 78 79 It("should return 200 in response when delete succeeds", func() { 80 81 response := handler.Handle(context.TODO(), Request{ 82 AdmissionRequest: admissionv1.AdmissionRequest{ 83 Operation: admissionv1.Delete, 84 OldObject: runtime.RawExtension{ 85 Raw: []byte("{}"), 86 Object: handler.validator, 87 }, 88 }, 89 }) 90 Expect(response.Allowed).Should(BeTrue()) 91 Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) 92 }) 93 }) 94 95 const warningMessage = "warning message" 96 const anotherWarningMessage = "another warning message" 97 Context("when dealing with successful results with warning", func() { 98 f := &fakeValidator{ErrorToReturn: nil, GVKToReturn: fakeValidatorVK, WarningsToReturn: []string{ 99 warningMessage, 100 anotherWarningMessage, 101 }} 102 handler := validatingHandler{validator: f, decoder: decoder} 103 104 It("should return 200 in response when create succeeds, with warning messages", func() { 105 response := handler.Handle(context.TODO(), Request{ 106 AdmissionRequest: admissionv1.AdmissionRequest{ 107 Operation: admissionv1.Create, 108 Object: runtime.RawExtension{ 109 Raw: []byte("{}"), 110 Object: handler.validator, 111 }, 112 }, 113 }) 114 115 Expect(response.Allowed).Should(BeTrue()) 116 Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) 117 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(warningMessage)) 118 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) 119 }) 120 121 It("should return 200 in response when update succeeds, with warning messages", func() { 122 123 response := handler.Handle(context.TODO(), Request{ 124 AdmissionRequest: admissionv1.AdmissionRequest{ 125 Operation: admissionv1.Update, 126 Object: runtime.RawExtension{ 127 Raw: []byte("{}"), 128 Object: handler.validator, 129 }, 130 OldObject: runtime.RawExtension{ 131 Raw: []byte("{}"), 132 Object: handler.validator, 133 }, 134 }, 135 }) 136 Expect(response.Allowed).Should(BeTrue()) 137 Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) 138 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(warningMessage)) 139 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) 140 }) 141 142 It("should return 200 in response when delete succeeds, with warning messages", func() { 143 144 response := handler.Handle(context.TODO(), Request{ 145 AdmissionRequest: admissionv1.AdmissionRequest{ 146 Operation: admissionv1.Delete, 147 OldObject: runtime.RawExtension{ 148 Raw: []byte("{}"), 149 Object: handler.validator, 150 }, 151 }, 152 }) 153 Expect(response.Allowed).Should(BeTrue()) 154 Expect(response.Result.Code).Should(Equal(int32(http.StatusOK))) 155 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(warningMessage)) 156 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) 157 }) 158 }) 159 160 Context("when dealing with Status errors, with warning messages", func() { 161 // Status error would overwrite the warning messages, so no warning messages should be observed. 162 expectedError := &apierrors.StatusError{ 163 ErrStatus: metav1.Status{ 164 Message: "some message", 165 Code: http.StatusUnprocessableEntity, 166 }, 167 } 168 f := &fakeValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK, WarningsToReturn: []string{warningMessage, anotherWarningMessage}} 169 handler := validatingHandler{validator: f, decoder: decoder} 170 171 It("should propagate the Status from ValidateCreate's return value to the HTTP response", func() { 172 173 response := handler.Handle(context.TODO(), Request{ 174 AdmissionRequest: admissionv1.AdmissionRequest{ 175 Operation: admissionv1.Create, 176 Object: runtime.RawExtension{ 177 Raw: []byte("{}"), 178 Object: handler.validator, 179 }, 180 }, 181 }) 182 183 Expect(response.Allowed).Should(BeFalse()) 184 Expect(response.Result.Code).Should(Equal(expectedError.Status().Code)) 185 Expect(*response.Result).Should(Equal(expectedError.Status())) 186 Expect(response.AdmissionResponse.Warnings).Should(ContainElements(warningMessage)) 187 Expect(response.AdmissionResponse.Warnings).Should(ContainElements(anotherWarningMessage)) 188 189 }) 190 191 It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func() { 192 193 response := handler.Handle(context.TODO(), Request{ 194 AdmissionRequest: admissionv1.AdmissionRequest{ 195 Operation: admissionv1.Update, 196 Object: runtime.RawExtension{ 197 Raw: []byte("{}"), 198 Object: handler.validator, 199 }, 200 OldObject: runtime.RawExtension{ 201 Raw: []byte("{}"), 202 Object: handler.validator, 203 }, 204 }, 205 }) 206 207 Expect(response.Allowed).Should(BeFalse()) 208 Expect(response.Result.Code).Should(Equal(expectedError.Status().Code)) 209 Expect(*response.Result).Should(Equal(expectedError.Status())) 210 Expect(response.AdmissionResponse.Warnings).Should(ContainElements(warningMessage)) 211 Expect(response.AdmissionResponse.Warnings).Should(ContainElements(anotherWarningMessage)) 212 213 }) 214 215 It("should propagate the Status from ValidateDelete's return value to the HTTP response", func() { 216 217 response := handler.Handle(context.TODO(), Request{ 218 AdmissionRequest: admissionv1.AdmissionRequest{ 219 Operation: admissionv1.Delete, 220 OldObject: runtime.RawExtension{ 221 Raw: []byte("{}"), 222 Object: handler.validator, 223 }, 224 }, 225 }) 226 227 Expect(response.Allowed).Should(BeFalse()) 228 Expect(response.Result.Code).Should(Equal(expectedError.Status().Code)) 229 Expect(*response.Result).Should(Equal(expectedError.Status())) 230 Expect(response.AdmissionResponse.Warnings).Should(ContainElements(warningMessage)) 231 Expect(response.AdmissionResponse.Warnings).Should(ContainElements(anotherWarningMessage)) 232 233 }) 234 235 }) 236 237 Context("when dealing with Status errors, without warning messages", func() { 238 239 expectedError := &apierrors.StatusError{ 240 ErrStatus: metav1.Status{ 241 Message: "some message", 242 Code: http.StatusUnprocessableEntity, 243 }, 244 } 245 f := &fakeValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK, WarningsToReturn: nil} 246 handler := validatingHandler{validator: f, decoder: decoder} 247 248 It("should propagate the Status from ValidateCreate's return value to the HTTP response", func() { 249 250 response := handler.Handle(context.TODO(), Request{ 251 AdmissionRequest: admissionv1.AdmissionRequest{ 252 Operation: admissionv1.Create, 253 Object: runtime.RawExtension{ 254 Raw: []byte("{}"), 255 Object: handler.validator, 256 }, 257 }, 258 }) 259 260 Expect(response.Allowed).Should(BeFalse()) 261 Expect(response.Result.Code).Should(Equal(expectedError.Status().Code)) 262 Expect(*response.Result).Should(Equal(expectedError.Status())) 263 264 }) 265 266 It("should propagate the Status from ValidateUpdate's return value to the HTTP response", func() { 267 268 response := handler.Handle(context.TODO(), Request{ 269 AdmissionRequest: admissionv1.AdmissionRequest{ 270 Operation: admissionv1.Update, 271 Object: runtime.RawExtension{ 272 Raw: []byte("{}"), 273 Object: handler.validator, 274 }, 275 OldObject: runtime.RawExtension{ 276 Raw: []byte("{}"), 277 Object: handler.validator, 278 }, 279 }, 280 }) 281 282 Expect(response.Allowed).Should(BeFalse()) 283 Expect(response.Result.Code).Should(Equal(expectedError.Status().Code)) 284 Expect(*response.Result).Should(Equal(expectedError.Status())) 285 286 }) 287 288 It("should propagate the Status from ValidateDelete's return value to the HTTP response", func() { 289 290 response := handler.Handle(context.TODO(), Request{ 291 AdmissionRequest: admissionv1.AdmissionRequest{ 292 Operation: admissionv1.Delete, 293 OldObject: runtime.RawExtension{ 294 Raw: []byte("{}"), 295 Object: handler.validator, 296 }, 297 }, 298 }) 299 300 Expect(response.Allowed).Should(BeFalse()) 301 Expect(response.Result.Code).Should(Equal(expectedError.Status().Code)) 302 Expect(*response.Result).Should(Equal(expectedError.Status())) 303 304 }) 305 306 }) 307 308 Context("when dealing with non-status errors, without warning messages", func() { 309 310 expectedError := errors.New("some error") 311 f := &fakeValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK} 312 handler := validatingHandler{validator: f, decoder: decoder} 313 314 It("should return 403 response when ValidateCreate with error message embedded", func() { 315 316 response := handler.Handle(context.TODO(), Request{ 317 AdmissionRequest: admissionv1.AdmissionRequest{ 318 Operation: admissionv1.Create, 319 Object: runtime.RawExtension{ 320 Raw: []byte("{}"), 321 Object: handler.validator, 322 }, 323 }, 324 }) 325 Expect(response.Allowed).Should(BeFalse()) 326 Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden))) 327 Expect(response.Result.Reason).Should(Equal(metav1.StatusReasonForbidden)) 328 Expect(response.Result.Message).Should(Equal(expectedError.Error())) 329 330 }) 331 332 It("should return 403 response when ValidateUpdate returns non-APIStatus error", func() { 333 334 response := handler.Handle(context.TODO(), Request{ 335 AdmissionRequest: admissionv1.AdmissionRequest{ 336 Operation: admissionv1.Update, 337 Object: runtime.RawExtension{ 338 Raw: []byte("{}"), 339 Object: handler.validator, 340 }, 341 OldObject: runtime.RawExtension{ 342 Raw: []byte("{}"), 343 Object: handler.validator, 344 }, 345 }, 346 }) 347 Expect(response.Allowed).Should(BeFalse()) 348 Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden))) 349 Expect(response.Result.Reason).Should(Equal(metav1.StatusReasonForbidden)) 350 Expect(response.Result.Message).Should(Equal(expectedError.Error())) 351 352 }) 353 354 It("should return 403 response when ValidateDelete returns non-APIStatus error", func() { 355 response := handler.Handle(context.TODO(), Request{ 356 AdmissionRequest: admissionv1.AdmissionRequest{ 357 Operation: admissionv1.Delete, 358 OldObject: runtime.RawExtension{ 359 Raw: []byte("{}"), 360 Object: handler.validator, 361 }, 362 }, 363 }) 364 Expect(response.Allowed).Should(BeFalse()) 365 Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden))) 366 Expect(response.Result.Reason).Should(Equal(metav1.StatusReasonForbidden)) 367 Expect(response.Result.Message).Should(Equal(expectedError.Error())) 368 }) 369 }) 370 371 Context("when dealing with non-status errors, with warning messages", func() { 372 373 expectedError := errors.New("some error") 374 f := &fakeValidator{ErrorToReturn: expectedError, GVKToReturn: fakeValidatorVK, WarningsToReturn: []string{warningMessage, anotherWarningMessage}} 375 handler := validatingHandler{validator: f, decoder: decoder} 376 377 It("should return 403 response when ValidateCreate with error message embedded", func() { 378 379 response := handler.Handle(context.TODO(), Request{ 380 AdmissionRequest: admissionv1.AdmissionRequest{ 381 Operation: admissionv1.Create, 382 Object: runtime.RawExtension{ 383 Raw: []byte("{}"), 384 Object: handler.validator, 385 }, 386 }, 387 }) 388 Expect(response.Allowed).Should(BeFalse()) 389 Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden))) 390 Expect(response.Result.Reason).Should(Equal(metav1.StatusReasonForbidden)) 391 Expect(response.Result.Message).Should(Equal(expectedError.Error())) 392 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(warningMessage)) 393 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) 394 }) 395 396 It("should return 403 response when ValidateUpdate returns non-APIStatus error", func() { 397 398 response := handler.Handle(context.TODO(), Request{ 399 AdmissionRequest: admissionv1.AdmissionRequest{ 400 Operation: admissionv1.Update, 401 Object: runtime.RawExtension{ 402 Raw: []byte("{}"), 403 Object: handler.validator, 404 }, 405 OldObject: runtime.RawExtension{ 406 Raw: []byte("{}"), 407 Object: handler.validator, 408 }, 409 }, 410 }) 411 Expect(response.Allowed).Should(BeFalse()) 412 Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden))) 413 Expect(response.Result.Reason).Should(Equal(metav1.StatusReasonForbidden)) 414 Expect(response.Result.Message).Should(Equal(expectedError.Error())) 415 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(warningMessage)) 416 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) 417 418 }) 419 420 It("should return 403 response when ValidateDelete returns non-APIStatus error", func() { 421 response := handler.Handle(context.TODO(), Request{ 422 AdmissionRequest: admissionv1.AdmissionRequest{ 423 Operation: admissionv1.Delete, 424 OldObject: runtime.RawExtension{ 425 Raw: []byte("{}"), 426 Object: handler.validator, 427 }, 428 }, 429 }) 430 Expect(response.Allowed).Should(BeFalse()) 431 Expect(response.Result.Code).Should(Equal(int32(http.StatusForbidden))) 432 Expect(response.Result.Reason).Should(Equal(metav1.StatusReasonForbidden)) 433 Expect(response.Result.Message).Should(Equal(expectedError.Error())) 434 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(warningMessage)) 435 Expect(response.AdmissionResponse.Warnings).Should(ContainElement(anotherWarningMessage)) 436 437 }) 438 }) 439 440 PIt("should return 400 in response when create fails on decode", func() {}) 441 442 PIt("should return 400 in response when update fails on decoding new object", func() {}) 443 444 PIt("should return 400 in response when update fails on decoding old object", func() {}) 445 446 PIt("should return 400 in response when delete fails on decode", func() {}) 447 448 }) 449 450 // fakeValidator provides fake validating webhook functionality for testing 451 // It implements the admission.Validator interface and 452 // rejects all requests with the same configured error 453 // or passes if ErrorToReturn is nil. 454 // And it would always return configured warning messages WarningsToReturn. 455 type fakeValidator struct { 456 // ErrorToReturn is the error for which the fakeValidator rejects all requests 457 ErrorToReturn error `json:"errorToReturn,omitempty"` 458 // GVKToReturn is the GroupVersionKind that the webhook operates on 459 GVKToReturn schema.GroupVersionKind 460 // WarningsToReturn is the warnings for fakeValidator returns to all requests 461 WarningsToReturn []string 462 } 463 464 func (v *fakeValidator) ValidateCreate() (warnings Warnings, err error) { 465 return v.WarningsToReturn, v.ErrorToReturn 466 } 467 468 func (v *fakeValidator) ValidateUpdate(old runtime.Object) (warnings Warnings, err error) { 469 return v.WarningsToReturn, v.ErrorToReturn 470 } 471 472 func (v *fakeValidator) ValidateDelete() (warnings Warnings, err error) { 473 return v.WarningsToReturn, v.ErrorToReturn 474 } 475 476 func (v *fakeValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) { 477 v.GVKToReturn = gvk 478 } 479 480 func (v *fakeValidator) GroupVersionKind() schema.GroupVersionKind { 481 return v.GVKToReturn 482 } 483 484 func (v *fakeValidator) GetObjectKind() schema.ObjectKind { 485 return v 486 } 487 488 func (v *fakeValidator) DeepCopyObject() runtime.Object { 489 return &fakeValidator{ 490 ErrorToReturn: v.ErrorToReturn, 491 GVKToReturn: v.GVKToReturn, 492 WarningsToReturn: v.WarningsToReturn, 493 } 494 }