sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/admission/http.go (about) 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{}