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 "errors" 22 "fmt" 23 "net/http" 24 "sync" 25 26 "github.com/go-logr/logr" 27 "gomodules.xyz/jsonpatch/v2" 28 admissionv1 "k8s.io/api/admission/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/util/json" 31 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 "k8s.io/klog/v2" 33 34 logf "sigs.k8s.io/controller-runtime/pkg/log" 35 "sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics" 36 ) 37 38 var ( 39 errUnableToEncodeResponse = errors.New("unable to encode response") 40 ) 41 42 // Request defines the input for an admission handler. 43 // It contains information to identify the object in 44 // question (group, version, kind, resource, subresource, 45 // name, namespace), as well as the operation in question 46 // (e.g. Get, Create, etc), and the object itself. 47 type Request struct { 48 admissionv1.AdmissionRequest 49 } 50 51 // Response is the output of an admission handler. 52 // It contains a response indicating if a given 53 // operation is allowed, as well as a set of patches 54 // to mutate the object in the case of a mutating admission handler. 55 type Response struct { 56 // Patches are the JSON patches for mutating webhooks. 57 // Using this instead of setting Response.Patch to minimize 58 // overhead of serialization and deserialization. 59 // Patches set here will override any patches in the response, 60 // so leave this empty if you want to set the patch response directly. 61 Patches []jsonpatch.JsonPatchOperation 62 // AdmissionResponse is the raw admission response. 63 // The Patch field in it will be overwritten by the listed patches. 64 admissionv1.AdmissionResponse 65 } 66 67 // Complete populates any fields that are yet to be set in 68 // the underlying AdmissionResponse, It mutates the response. 69 func (r *Response) Complete(req Request) error { 70 r.UID = req.UID 71 72 // ensure that we have a valid status code 73 if r.Result == nil { 74 r.Result = &metav1.Status{} 75 } 76 if r.Result.Code == 0 { 77 r.Result.Code = http.StatusOK 78 } 79 // TODO(directxman12): do we need to populate this further, and/or 80 // is code actually necessary (the same webhook doesn't use it) 81 82 if len(r.Patches) == 0 { 83 return nil 84 } 85 86 var err error 87 r.Patch, err = json.Marshal(r.Patches) 88 if err != nil { 89 return err 90 } 91 patchType := admissionv1.PatchTypeJSONPatch 92 r.PatchType = &patchType 93 94 return nil 95 } 96 97 // Handler can handle an AdmissionRequest. 98 type Handler interface { 99 // Handle yields a response to an AdmissionRequest. 100 // 101 // The supplied context is extracted from the received http.Request, allowing wrapping 102 // http.Handlers to inject values into and control cancelation of downstream request processing. 103 Handle(context.Context, Request) Response 104 } 105 106 // HandlerFunc implements Handler interface using a single function. 107 type HandlerFunc func(context.Context, Request) Response 108 109 var _ Handler = HandlerFunc(nil) 110 111 // Handle process the AdmissionRequest by invoking the underlying function. 112 func (f HandlerFunc) Handle(ctx context.Context, req Request) Response { 113 return f(ctx, req) 114 } 115 116 // Webhook represents each individual webhook. 117 // 118 // It must be registered with a webhook.Server or 119 // populated by StandaloneWebhook to be ran on an arbitrary HTTP server. 120 type Webhook struct { 121 // Handler actually processes an admission request returning whether it was allowed or denied, 122 // and potentially patches to apply to the handler. 123 Handler Handler 124 125 // RecoverPanic indicates whether the panic caused by webhook should be recovered. 126 RecoverPanic bool 127 128 // WithContextFunc will allow you to take the http.Request.Context() and 129 // add any additional information such as passing the request path or 130 // headers thus allowing you to read them from within the handler 131 WithContextFunc func(context.Context, *http.Request) context.Context 132 133 // LogConstructor is used to construct a logger for logging messages during webhook calls 134 // based on the given base logger (which might carry more values like the webhook's path). 135 // Note: LogConstructor has to be able to handle nil requests as we are also using it 136 // outside the context of requests. 137 LogConstructor func(base logr.Logger, req *Request) logr.Logger 138 139 setupLogOnce sync.Once 140 log logr.Logger 141 } 142 143 // WithRecoverPanic takes a bool flag which indicates whether the panic caused by webhook should be recovered. 144 func (wh *Webhook) WithRecoverPanic(recoverPanic bool) *Webhook { 145 wh.RecoverPanic = recoverPanic 146 return wh 147 } 148 149 // Handle processes AdmissionRequest. 150 // If the webhook is mutating type, it delegates the AdmissionRequest to each handler and merge the patches. 151 // If the webhook is validating type, it delegates the AdmissionRequest to each handler and 152 // deny the request if anyone denies. 153 func (wh *Webhook) Handle(ctx context.Context, req Request) (response Response) { 154 if wh.RecoverPanic { 155 defer func() { 156 if r := recover(); r != nil { 157 for _, fn := range utilruntime.PanicHandlers { 158 fn(r) 159 } 160 response = Errored(http.StatusInternalServerError, fmt.Errorf("panic: %v [recovered]", r)) 161 return 162 } 163 }() 164 } 165 166 reqLog := wh.getLogger(&req) 167 ctx = logf.IntoContext(ctx, reqLog) 168 169 resp := wh.Handler.Handle(ctx, req) 170 if err := resp.Complete(req); err != nil { 171 reqLog.Error(err, "unable to encode response") 172 return Errored(http.StatusInternalServerError, errUnableToEncodeResponse) 173 } 174 175 return resp 176 } 177 178 // getLogger constructs a logger from the injected log and LogConstructor. 179 func (wh *Webhook) getLogger(req *Request) logr.Logger { 180 wh.setupLogOnce.Do(func() { 181 if wh.log.GetSink() == nil { 182 wh.log = logf.Log.WithName("admission") 183 } 184 }) 185 186 logConstructor := wh.LogConstructor 187 if logConstructor == nil { 188 logConstructor = DefaultLogConstructor 189 } 190 return logConstructor(wh.log, req) 191 } 192 193 // DefaultLogConstructor adds some commonly interesting fields to the given logger. 194 func DefaultLogConstructor(base logr.Logger, req *Request) logr.Logger { 195 if req != nil { 196 return base.WithValues("object", klog.KRef(req.Namespace, req.Name), 197 "namespace", req.Namespace, "name", req.Name, 198 "resource", req.Resource, "user", req.UserInfo.Username, 199 "requestID", req.UID, 200 ) 201 } 202 return base 203 } 204 205 // StandaloneOptions let you configure a StandaloneWebhook. 206 type StandaloneOptions struct { 207 // Logger to be used by the webhook. 208 // If none is set, it defaults to log.Log global logger. 209 Logger logr.Logger 210 // MetricsPath is used for labelling prometheus metrics 211 // by the path is served on. 212 // If none is set, prometheus metrics will not be generated. 213 MetricsPath string 214 } 215 216 // StandaloneWebhook prepares a webhook for use without a webhook.Server, 217 // passing in the information normally populated by webhook.Server 218 // and instrumenting the webhook with metrics. 219 // 220 // Use this to attach your webhook to an arbitrary HTTP server or mux. 221 // 222 // Note that you are responsible for terminating TLS if you use StandaloneWebhook 223 // in your own server/mux. In order to be accessed by a kubernetes cluster, 224 // all webhook servers require TLS. 225 func StandaloneWebhook(hook *Webhook, opts StandaloneOptions) (http.Handler, error) { 226 if opts.Logger.GetSink() != nil { 227 hook.log = opts.Logger 228 } 229 if opts.MetricsPath == "" { 230 return hook, nil 231 } 232 return metrics.InstrumentedHook(opts.MetricsPath, hook), nil 233 } 234 235 // requestContextKey is how we find the admission.Request in a context.Context. 236 type requestContextKey struct{} 237 238 // RequestFromContext returns an admission.Request from ctx. 239 func RequestFromContext(ctx context.Context) (Request, error) { 240 if v, ok := ctx.Value(requestContextKey{}).(Request); ok { 241 return v, nil 242 } 243 244 return Request{}, errors.New("admission.Request not found in context") 245 } 246 247 // NewContextWithRequest returns a new Context, derived from ctx, which carries the 248 // provided admission.Request. 249 func NewContextWithRequest(ctx context.Context, req Request) context.Context { 250 return context.WithValue(ctx, requestContextKey{}, req) 251 } 252