...

Source file src/sigs.k8s.io/controller-runtime/pkg/webhook/admission/validator_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/webhook/admission

     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  }
   495  

View as plain text