...

Source file src/sigs.k8s.io/controller-runtime/pkg/webhook/admission/webhook.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  	"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  

View as plain text