...

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

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

     1  /*
     2  Copyright 2018 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  	"io"
    22  	"net/http"
    23  
    24  	"github.com/go-logr/logr"
    25  	. "github.com/onsi/ginkgo/v2"
    26  	. "github.com/onsi/gomega"
    27  	"github.com/onsi/gomega/gbytes"
    28  	"gomodules.xyz/jsonpatch/v2"
    29  	admissionv1 "k8s.io/api/admission/v1"
    30  	authenticationv1 "k8s.io/api/authentication/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	machinerytypes "k8s.io/apimachinery/pkg/types"
    33  
    34  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    35  	"sigs.k8s.io/controller-runtime/pkg/log/zap"
    36  )
    37  
    38  var _ = Describe("Admission Webhooks", func() {
    39  	var (
    40  		logBuffer  *gbytes.Buffer
    41  		testLogger logr.Logger
    42  	)
    43  
    44  	BeforeEach(func() {
    45  		logBuffer = gbytes.NewBuffer()
    46  		testLogger = zap.New(zap.JSONEncoder(), zap.WriteTo(io.MultiWriter(logBuffer, GinkgoWriter)))
    47  	})
    48  
    49  	allowHandler := func() *Webhook {
    50  		handler := &fakeHandler{
    51  			fn: func(ctx context.Context, req Request) Response {
    52  				return Response{
    53  					AdmissionResponse: admissionv1.AdmissionResponse{
    54  						Allowed: true,
    55  					},
    56  				}
    57  			},
    58  		}
    59  		webhook := &Webhook{
    60  			Handler: handler,
    61  		}
    62  
    63  		return webhook
    64  	}
    65  
    66  	It("should invoke the handler to get a response", func() {
    67  		By("setting up a webhook with an allow handler")
    68  		webhook := allowHandler()
    69  
    70  		By("invoking the webhook")
    71  		resp := webhook.Handle(context.Background(), Request{})
    72  
    73  		By("checking that it allowed the request")
    74  		Expect(resp.Allowed).To(BeTrue())
    75  	})
    76  
    77  	It("should ensure that the response's UID is set to the request's UID", func() {
    78  		By("setting up a webhook")
    79  		webhook := allowHandler()
    80  
    81  		By("invoking the webhook")
    82  		resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{UID: "foobar"}})
    83  
    84  		By("checking that the response share's the request's UID")
    85  		Expect(resp.UID).To(Equal(machinerytypes.UID("foobar")))
    86  	})
    87  
    88  	It("should populate the status on a response if one is not provided", func() {
    89  		By("setting up a webhook")
    90  		webhook := allowHandler()
    91  
    92  		By("invoking the webhook")
    93  		resp := webhook.Handle(context.Background(), Request{})
    94  
    95  		By("checking that the response share's the request's UID")
    96  		Expect(resp.Result).To(Equal(&metav1.Status{Code: http.StatusOK}))
    97  	})
    98  
    99  	It("shouldn't overwrite the status on a response", func() {
   100  		By("setting up a webhook that sets a status")
   101  		webhook := &Webhook{
   102  			Handler: HandlerFunc(func(ctx context.Context, req Request) Response {
   103  				return Response{
   104  					AdmissionResponse: admissionv1.AdmissionResponse{
   105  						Allowed: true,
   106  						Result:  &metav1.Status{Message: "Ground Control to Major Tom"},
   107  					},
   108  				}
   109  			}),
   110  		}
   111  
   112  		By("invoking the webhook")
   113  		resp := webhook.Handle(context.Background(), Request{})
   114  
   115  		By("checking that the message is intact")
   116  		Expect(resp.Result).NotTo(BeNil())
   117  		Expect(resp.Result.Message).To(Equal("Ground Control to Major Tom"))
   118  	})
   119  
   120  	It("should serialize patch operations into a single jsonpatch blob", func() {
   121  		By("setting up a webhook with a patching handler")
   122  		webhook := &Webhook{
   123  			Handler: HandlerFunc(func(ctx context.Context, req Request) Response {
   124  				return Patched("", jsonpatch.Operation{Operation: "add", Path: "/a", Value: 2}, jsonpatch.Operation{Operation: "replace", Path: "/b", Value: 4})
   125  			}),
   126  		}
   127  
   128  		By("invoking the webhook")
   129  		resp := webhook.Handle(context.Background(), Request{})
   130  
   131  		By("checking that a JSON patch is populated on the response")
   132  		patchType := admissionv1.PatchTypeJSONPatch
   133  		Expect(resp.PatchType).To(Equal(&patchType))
   134  		Expect(resp.Patch).To(Equal([]byte(`[{"op":"add","path":"/a","value":2},{"op":"replace","path":"/b","value":4}]`)))
   135  	})
   136  
   137  	It("should pass a request logger via the context", func() {
   138  		By("setting up a webhook that uses the request logger")
   139  		webhook := &Webhook{
   140  			Handler: HandlerFunc(func(ctx context.Context, req Request) Response {
   141  				logf.FromContext(ctx).Info("Received request")
   142  
   143  				return Response{
   144  					AdmissionResponse: admissionv1.AdmissionResponse{
   145  						Allowed: true,
   146  					},
   147  				}
   148  			}),
   149  			log: testLogger,
   150  		}
   151  
   152  		By("invoking the webhook")
   153  		resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{
   154  			UID:       "test123",
   155  			Name:      "foo",
   156  			Namespace: "bar",
   157  			Resource: metav1.GroupVersionResource{
   158  				Group:    "apps",
   159  				Version:  "v1",
   160  				Resource: "deployments",
   161  			},
   162  			UserInfo: authenticationv1.UserInfo{
   163  				Username: "tim",
   164  			},
   165  		}})
   166  		Expect(resp.Allowed).To(BeTrue())
   167  
   168  		By("checking that the log message contains the request fields")
   169  		Eventually(logBuffer).Should(gbytes.Say(`"msg":"Received request","object":{"name":"foo","namespace":"bar"},"namespace":"bar","name":"foo","resource":{"group":"apps","version":"v1","resource":"deployments"},"user":"tim","requestID":"test123"}`))
   170  	})
   171  
   172  	It("should pass a request logger created by LogConstructor via the context", func() {
   173  		By("setting up a webhook that uses the request logger")
   174  		webhook := &Webhook{
   175  			Handler: HandlerFunc(func(ctx context.Context, req Request) Response {
   176  				logf.FromContext(ctx).Info("Received request")
   177  
   178  				return Response{
   179  					AdmissionResponse: admissionv1.AdmissionResponse{
   180  						Allowed: true,
   181  					},
   182  				}
   183  			}),
   184  			LogConstructor: func(base logr.Logger, req *Request) logr.Logger {
   185  				return base.WithValues("operation", req.Operation, "requestID", req.UID)
   186  			},
   187  			log: testLogger,
   188  		}
   189  
   190  		By("invoking the webhook")
   191  		resp := webhook.Handle(context.Background(), Request{AdmissionRequest: admissionv1.AdmissionRequest{
   192  			UID:       "test123",
   193  			Operation: admissionv1.Create,
   194  		}})
   195  		Expect(resp.Allowed).To(BeTrue())
   196  
   197  		By("checking that the log message contains the request fields")
   198  		Eventually(logBuffer).Should(gbytes.Say(`"msg":"Received request","operation":"CREATE","requestID":"test123"}`))
   199  	})
   200  
   201  	Describe("panic recovery", func() {
   202  		It("should recover panic if RecoverPanic is true", func() {
   203  			panicHandler := func() *Webhook {
   204  				handler := &fakeHandler{
   205  					fn: func(ctx context.Context, req Request) Response {
   206  						panic("fake panic test")
   207  					},
   208  				}
   209  				webhook := &Webhook{
   210  					Handler:      handler,
   211  					RecoverPanic: true,
   212  				}
   213  
   214  				return webhook
   215  			}
   216  
   217  			By("setting up a webhook with a panicking handler")
   218  			webhook := panicHandler()
   219  
   220  			By("invoking the webhook")
   221  			resp := webhook.Handle(context.Background(), Request{})
   222  
   223  			By("checking that it errored the request")
   224  			Expect(resp.Allowed).To(BeFalse())
   225  			Expect(resp.Result.Code).To(Equal(int32(http.StatusInternalServerError)))
   226  			Expect(resp.Result.Message).To(Equal("panic: fake panic test [recovered]"))
   227  		})
   228  
   229  		It("should not recover panic if RecoverPanic is false by default", func() {
   230  			panicHandler := func() *Webhook {
   231  				handler := &fakeHandler{
   232  					fn: func(ctx context.Context, req Request) Response {
   233  						panic("fake panic test")
   234  					},
   235  				}
   236  				webhook := &Webhook{
   237  					Handler: handler,
   238  				}
   239  
   240  				return webhook
   241  			}
   242  
   243  			By("setting up a webhook with a panicking handler")
   244  			defer func() {
   245  				Expect(recover()).ShouldNot(BeNil())
   246  			}()
   247  			webhook := panicHandler()
   248  
   249  			By("invoking the webhook")
   250  			webhook.Handle(context.Background(), Request{})
   251  		})
   252  	})
   253  })
   254  
   255  var _ = Describe("Should be able to write/read admission.Request to/from context", func() {
   256  	ctx := context.Background()
   257  	testRequest := Request{
   258  		admissionv1.AdmissionRequest{
   259  			UID: "test-uid",
   260  		},
   261  	}
   262  
   263  	ctx = NewContextWithRequest(ctx, testRequest)
   264  
   265  	gotRequest, err := RequestFromContext(ctx)
   266  	Expect(err).To(Not(HaveOccurred()))
   267  	Expect(gotRequest).To(Equal(testRequest))
   268  })
   269  

View as plain text