...

Source file src/sigs.k8s.io/controller-runtime/pkg/webhook/admission/http_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  	"bytes"
    21  	"context"
    22  	"crypto/rand"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"time"
    28  
    29  	. "github.com/onsi/ginkgo/v2"
    30  	. "github.com/onsi/gomega"
    31  
    32  	admissionv1 "k8s.io/api/admission/v1"
    33  )
    34  
    35  var _ = Describe("Admission Webhooks", func() {
    36  
    37  	const (
    38  		gvkJSONv1      = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1"`
    39  		gvkJSONv1beta1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1beta1"`
    40  	)
    41  
    42  	Describe("HTTP Handler", func() {
    43  		var respRecorder *httptest.ResponseRecorder
    44  		webhook := &Webhook{
    45  			Handler: nil,
    46  		}
    47  		BeforeEach(func() {
    48  			respRecorder = &httptest.ResponseRecorder{
    49  				Body: bytes.NewBuffer(nil),
    50  			}
    51  		})
    52  
    53  		It("should return bad-request when given an empty body", func() {
    54  			req := &http.Request{Body: nil}
    55  
    56  			expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
    57  `
    58  			webhook.ServeHTTP(respRecorder, req)
    59  			Expect(respRecorder.Body.String()).To(Equal(expected))
    60  		})
    61  
    62  		It("should return bad-request when given the wrong content-type", func() {
    63  			req := &http.Request{
    64  				Header: http.Header{"Content-Type": []string{"application/foo"}},
    65  				Body:   nopCloser{Reader: bytes.NewBuffer(nil)},
    66  			}
    67  
    68  			expected :=
    69  				`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"contentType=application/foo, expected application/json","code":400}}}
    70  `
    71  			webhook.ServeHTTP(respRecorder, req)
    72  			Expect(respRecorder.Body.String()).To(Equal(expected))
    73  		})
    74  
    75  		It("should return bad-request when given an undecodable body", func() {
    76  			req := &http.Request{
    77  				Header: http.Header{"Content-Type": []string{"application/json"}},
    78  				Body:   nopCloser{Reader: bytes.NewBufferString("{")},
    79  			}
    80  
    81  			expected :=
    82  				`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"couldn't get version/kind; json parse error: unexpected end of JSON input","code":400}}}
    83  `
    84  			webhook.ServeHTTP(respRecorder, req)
    85  			Expect(respRecorder.Body.String()).To(Equal(expected))
    86  		})
    87  
    88  		It("should error when given a NoBody", func() {
    89  			req := &http.Request{
    90  				Header: http.Header{"Content-Type": []string{"application/json"}},
    91  				Method: http.MethodPost,
    92  				Body:   http.NoBody,
    93  			}
    94  
    95  			expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}}
    96  `
    97  			webhook.ServeHTTP(respRecorder, req)
    98  			Expect(respRecorder.Body.String()).To(Equal(expected))
    99  		})
   100  
   101  		It("should error when given an infinite body", func() {
   102  			req := &http.Request{
   103  				Header: http.Header{"Content-Type": []string{"application/json"}},
   104  				Method: http.MethodPost,
   105  				Body:   nopCloser{Reader: rand.Reader},
   106  			}
   107  
   108  			expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request entity is too large; limit is 7340032 bytes","code":413}}}
   109  `
   110  			webhook.ServeHTTP(respRecorder, req)
   111  			Expect(respRecorder.Body.String()).To(Equal(expected))
   112  		})
   113  
   114  		It("should return the response given by the handler with version defaulted to v1", func() {
   115  			req := &http.Request{
   116  				Header: http.Header{"Content-Type": []string{"application/json"}},
   117  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
   118  			}
   119  			webhook := &Webhook{
   120  				Handler: &fakeHandler{},
   121  			}
   122  
   123  			expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
   124  `, gvkJSONv1)
   125  			webhook.ServeHTTP(respRecorder, req)
   126  			Expect(respRecorder.Body.String()).To(Equal(expected))
   127  		})
   128  
   129  		It("should return the v1 response given by the handler", func() {
   130  			req := &http.Request{
   131  				Header: http.Header{"Content-Type": []string{"application/json"}},
   132  				Body:   nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))},
   133  			}
   134  			webhook := &Webhook{
   135  				Handler: &fakeHandler{},
   136  			}
   137  
   138  			expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
   139  `, gvkJSONv1)
   140  			webhook.ServeHTTP(respRecorder, req)
   141  			Expect(respRecorder.Body.String()).To(Equal(expected))
   142  		})
   143  
   144  		It("should return the v1beta1 response given by the handler", func() {
   145  			req := &http.Request{
   146  				Header: http.Header{"Content-Type": []string{"application/json"}},
   147  				Body:   nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1beta1))},
   148  			}
   149  			webhook := &Webhook{
   150  				Handler: &fakeHandler{},
   151  			}
   152  
   153  			expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}}
   154  `, gvkJSONv1beta1)
   155  			webhook.ServeHTTP(respRecorder, req)
   156  			Expect(respRecorder.Body.String()).To(Equal(expected))
   157  		})
   158  
   159  		It("should present the Context from the HTTP request, if any", func() {
   160  			req := &http.Request{
   161  				Header: http.Header{"Content-Type": []string{"application/json"}},
   162  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
   163  			}
   164  			type ctxkey int
   165  			const key ctxkey = 1
   166  			const value = "from-ctx"
   167  			webhook := &Webhook{
   168  				Handler: &fakeHandler{
   169  					fn: func(ctx context.Context, req Request) Response {
   170  						<-ctx.Done()
   171  						return Allowed(ctx.Value(key).(string))
   172  					},
   173  				},
   174  			}
   175  
   176  			expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}}
   177  `, gvkJSONv1, value)
   178  
   179  			ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value))
   180  			cancel()
   181  			webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
   182  			Expect(respRecorder.Body.String()).To(Equal(expected))
   183  		})
   184  
   185  		It("should mutate the Context from the HTTP request, if func supplied", func() {
   186  			req := &http.Request{
   187  				Header: http.Header{"Content-Type": []string{"application/json"}},
   188  				Body:   nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)},
   189  			}
   190  			type ctxkey int
   191  			const key ctxkey = 1
   192  			webhook := &Webhook{
   193  				Handler: &fakeHandler{
   194  					fn: func(ctx context.Context, req Request) Response {
   195  						return Allowed(ctx.Value(key).(string))
   196  					},
   197  				},
   198  				WithContextFunc: func(ctx context.Context, r *http.Request) context.Context {
   199  					return context.WithValue(ctx, key, r.Header["Content-Type"][0])
   200  				},
   201  			}
   202  
   203  			expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}}
   204  `, gvkJSONv1, "application/json")
   205  
   206  			ctx, cancel := context.WithCancel(context.Background())
   207  			cancel()
   208  			webhook.ServeHTTP(respRecorder, req.WithContext(ctx))
   209  			Expect(respRecorder.Body.String()).To(Equal(expected))
   210  		})
   211  
   212  		It("should never run into circular calling if the writer has broken", func() {
   213  			req := &http.Request{
   214  				Header: http.Header{"Content-Type": []string{"application/json"}},
   215  				Body:   nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))},
   216  			}
   217  			webhook := &Webhook{
   218  				Handler: &fakeHandler{},
   219  			}
   220  
   221  			bw := &brokenWriter{ResponseWriter: respRecorder}
   222  			Eventually(func() int {
   223  				// This should not be blocked by the circular calling of writeResponse and writeAdmissionResponse
   224  				webhook.ServeHTTP(bw, req)
   225  				return respRecorder.Body.Len()
   226  			}, time.Second*3).Should(Equal(0))
   227  		})
   228  	})
   229  })
   230  
   231  type nopCloser struct {
   232  	io.Reader
   233  }
   234  
   235  func (nopCloser) Close() error { return nil }
   236  
   237  type fakeHandler struct {
   238  	invoked bool
   239  	fn      func(context.Context, Request) Response
   240  }
   241  
   242  func (h *fakeHandler) Handle(ctx context.Context, req Request) Response {
   243  	h.invoked = true
   244  	if h.fn != nil {
   245  		return h.fn(ctx, req)
   246  	}
   247  	return Response{AdmissionResponse: admissionv1.AdmissionResponse{
   248  		Allowed: true,
   249  	}}
   250  }
   251  
   252  type brokenWriter struct {
   253  	http.ResponseWriter
   254  }
   255  
   256  func (bw *brokenWriter) Write(buf []byte) (int, error) {
   257  	return 0, fmt.Errorf("mock: write: broken pipe")
   258  }
   259  

View as plain text