...

Source file src/sigs.k8s.io/controller-runtime/pkg/webhook/admission/http.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  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  
    26  	v1 "k8s.io/api/admission/v1"
    27  	"k8s.io/api/admission/v1beta1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apimachinery/pkg/runtime/serializer"
    31  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    32  )
    33  
    34  var admissionScheme = runtime.NewScheme()
    35  var admissionCodecs = serializer.NewCodecFactory(admissionScheme)
    36  
    37  // adapted from https://github.com/kubernetes/kubernetes/blob/c28c2009181fcc44c5f6b47e10e62dacf53e4da0/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go
    38  //
    39  // From https://github.com/kubernetes/apiserver/blob/d6876a0600de06fef75968c4641c64d7da499f25/pkg/server/config.go#L433-L442C5:
    40  //
    41  //	     1.5MB is the recommended client request size in byte
    42  //		 the etcd server should accept. See
    43  //		 https://github.com/etcd-io/etcd/blob/release-3.4/embed/config.go#L56.
    44  //		 A request body might be encoded in json, and is converted to
    45  //		 proto when persisted in etcd, so we allow 2x as the largest request
    46  //		 body size to be accepted and decoded in a write request.
    47  //
    48  // For the admission request, we can infer that it contains at most two objects
    49  // (the old and new versions of the object being admitted), each of which can
    50  // be at most 3MB in size. For the rest of the request, we can assume that
    51  // it will be less than 1MB in size. Therefore, we can set the max request
    52  // size to 7MB.
    53  // If your use case requires larger max request sizes, please
    54  // open an issue (https://github.com/kubernetes-sigs/controller-runtime/issues/new).
    55  const maxRequestSize = int64(7 * 1024 * 1024)
    56  
    57  func init() {
    58  	utilruntime.Must(v1.AddToScheme(admissionScheme))
    59  	utilruntime.Must(v1beta1.AddToScheme(admissionScheme))
    60  }
    61  
    62  var _ http.Handler = &Webhook{}
    63  
    64  func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    65  	ctx := r.Context()
    66  	if wh.WithContextFunc != nil {
    67  		ctx = wh.WithContextFunc(ctx, r)
    68  	}
    69  
    70  	if r.Body == nil || r.Body == http.NoBody {
    71  		err := errors.New("request body is empty")
    72  		wh.getLogger(nil).Error(err, "bad request")
    73  		wh.writeResponse(w, Errored(http.StatusBadRequest, err))
    74  		return
    75  	}
    76  
    77  	defer r.Body.Close()
    78  	limitedReader := &io.LimitedReader{R: r.Body, N: maxRequestSize}
    79  	body, err := io.ReadAll(limitedReader)
    80  	if err != nil {
    81  		wh.getLogger(nil).Error(err, "unable to read the body from the incoming request")
    82  		wh.writeResponse(w, Errored(http.StatusBadRequest, err))
    83  		return
    84  	}
    85  	if limitedReader.N <= 0 {
    86  		err := fmt.Errorf("request entity is too large; limit is %d bytes", maxRequestSize)
    87  		wh.getLogger(nil).Error(err, "unable to read the body from the incoming request; limit reached")
    88  		wh.writeResponse(w, Errored(http.StatusRequestEntityTooLarge, err))
    89  		return
    90  	}
    91  
    92  	// verify the content type is accurate
    93  	if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
    94  		err = fmt.Errorf("contentType=%s, expected application/json", contentType)
    95  		wh.getLogger(nil).Error(err, "unable to process a request with unknown content type")
    96  		wh.writeResponse(w, Errored(http.StatusBadRequest, err))
    97  		return
    98  	}
    99  
   100  	// Both v1 and v1beta1 AdmissionReview types are exactly the same, so the v1beta1 type can
   101  	// be decoded into the v1 type. However the runtime codec's decoder guesses which type to
   102  	// decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an
   103  	// unregistered type to the v1 GVK, the decoder will coerce a v1beta1 AdmissionReview to v1.
   104  	// The actual AdmissionReview GVK will be used to write a typed response in case the
   105  	// webhook config permits multiple versions, otherwise this response will fail.
   106  	req := Request{}
   107  	ar := unversionedAdmissionReview{}
   108  	// avoid an extra copy
   109  	ar.Request = &req.AdmissionRequest
   110  	ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
   111  	_, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar)
   112  	if err != nil {
   113  		wh.getLogger(nil).Error(err, "unable to decode the request")
   114  		wh.writeResponse(w, Errored(http.StatusBadRequest, err))
   115  		return
   116  	}
   117  	wh.getLogger(&req).V(5).Info("received request")
   118  
   119  	wh.writeResponseTyped(w, wh.Handle(ctx, req), actualAdmRevGVK)
   120  }
   121  
   122  // writeResponse writes response to w generically, i.e. without encoding GVK information.
   123  func (wh *Webhook) writeResponse(w io.Writer, response Response) {
   124  	wh.writeAdmissionResponse(w, v1.AdmissionReview{Response: &response.AdmissionResponse})
   125  }
   126  
   127  // writeResponseTyped writes response to w with GVK set to admRevGVK, which is necessary
   128  // if multiple AdmissionReview versions are permitted by the webhook.
   129  func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, admRevGVK *schema.GroupVersionKind) {
   130  	ar := v1.AdmissionReview{
   131  		Response: &response.AdmissionResponse,
   132  	}
   133  	// Default to a v1 AdmissionReview, otherwise the API server may not recognize the request
   134  	// if multiple AdmissionReview versions are permitted by the webhook config.
   135  	// TODO(estroz): this should be configurable since older API servers won't know about v1.
   136  	if admRevGVK == nil || *admRevGVK == (schema.GroupVersionKind{}) {
   137  		ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview"))
   138  	} else {
   139  		ar.SetGroupVersionKind(*admRevGVK)
   140  	}
   141  	wh.writeAdmissionResponse(w, ar)
   142  }
   143  
   144  // writeAdmissionResponse writes ar to w.
   145  func (wh *Webhook) writeAdmissionResponse(w io.Writer, ar v1.AdmissionReview) {
   146  	if err := json.NewEncoder(w).Encode(ar); err != nil {
   147  		wh.getLogger(nil).Error(err, "unable to encode and write the response")
   148  		// Since the `ar v1.AdmissionReview` is a clear and legal object,
   149  		// it should not have problem to be marshalled into bytes.
   150  		// The error here is probably caused by the abnormal HTTP connection,
   151  		// e.g., broken pipe, so we can only write the error response once,
   152  		// to avoid endless circular calling.
   153  		serverError := Errored(http.StatusInternalServerError, err)
   154  		if err = json.NewEncoder(w).Encode(v1.AdmissionReview{Response: &serverError.AdmissionResponse}); err != nil {
   155  			wh.getLogger(nil).Error(err, "still unable to encode and write the InternalServerError response")
   156  		}
   157  	} else {
   158  		res := ar.Response
   159  		if log := wh.getLogger(nil); log.V(5).Enabled() {
   160  			if res.Result != nil {
   161  				log = log.WithValues("code", res.Result.Code, "reason", res.Result.Reason, "message", res.Result.Message)
   162  			}
   163  			log.V(5).Info("wrote response", "requestID", res.UID, "allowed", res.Allowed)
   164  		}
   165  	}
   166  }
   167  
   168  // unversionedAdmissionReview is used to decode both v1 and v1beta1 AdmissionReview types.
   169  type unversionedAdmissionReview struct {
   170  	v1.AdmissionReview
   171  }
   172  
   173  var _ runtime.Object = &unversionedAdmissionReview{}
   174  

View as plain text