k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/validating/dispatcher.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 validating
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"sync"
    24  	"time"
    25  
    26  	"go.opentelemetry.io/otel/attribute"
    27  
    28  	v1 "k8s.io/api/admissionregistration/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    32  	"k8s.io/apiserver/pkg/admission"
    33  	admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
    34  	"k8s.io/apiserver/pkg/admission/plugin/webhook"
    35  	webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
    36  	"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
    37  	webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
    38  	endpointsrequest "k8s.io/apiserver/pkg/endpoints/request"
    39  	webhookutil "k8s.io/apiserver/pkg/util/webhook"
    40  	"k8s.io/apiserver/pkg/warning"
    41  	"k8s.io/component-base/tracing"
    42  	"k8s.io/klog/v2"
    43  )
    44  
    45  const (
    46  	// ValidatingAuditAnnotationPrefix is a prefix for keeping noteworthy
    47  	// validating audit annotations.
    48  	ValidatingAuditAnnotationPrefix = "validating.webhook.admission.k8s.io/"
    49  	// ValidatingAuditAnnotationFailedOpenKeyPrefix in an annotation indicates
    50  	// the validating webhook failed open when the webhook backend connection
    51  	// failed or returned an internal server error.
    52  	ValidatingAuditAnnotationFailedOpenKeyPrefix = "failed-open." + ValidatingAuditAnnotationPrefix
    53  )
    54  
    55  type validatingDispatcher struct {
    56  	cm     *webhookutil.ClientManager
    57  	plugin *Plugin
    58  }
    59  
    60  func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
    61  	return func(cm *webhookutil.ClientManager) generic.Dispatcher {
    62  		return &validatingDispatcher{cm, p}
    63  	}
    64  }
    65  
    66  var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{}
    67  
    68  type versionedAttributeAccessor struct {
    69  	versionedAttrs   map[schema.GroupVersionKind]*admission.VersionedAttributes
    70  	attr             admission.Attributes
    71  	objectInterfaces admission.ObjectInterfaces
    72  }
    73  
    74  func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
    75  	if val, ok := v.versionedAttrs[gvk]; ok {
    76  		return val, nil
    77  	}
    78  	versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	v.versionedAttrs[gvk] = versionedAttr
    83  	return versionedAttr, nil
    84  }
    85  
    86  var _ generic.Dispatcher = &validatingDispatcher{}
    87  
    88  func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
    89  	var relevantHooks []*generic.WebhookInvocation
    90  	// Construct all the versions we need to call our webhooks
    91  	versionedAttrAccessor := &versionedAttributeAccessor{
    92  		versionedAttrs:   map[schema.GroupVersionKind]*admission.VersionedAttributes{},
    93  		attr:             attr,
    94  		objectInterfaces: o,
    95  	}
    96  	for _, hook := range hooks {
    97  		invocation, statusError := d.plugin.ShouldCallHook(ctx, hook, attr, o, versionedAttrAccessor)
    98  		if statusError != nil {
    99  			return statusError
   100  		}
   101  		if invocation == nil {
   102  			continue
   103  		}
   104  
   105  		relevantHooks = append(relevantHooks, invocation)
   106  		// VersionedAttr result will be cached and reused later during parallel webhook calls
   107  		_, err := versionedAttrAccessor.VersionedAttribute(invocation.Kind)
   108  		if err != nil {
   109  			return apierrors.NewInternalError(err)
   110  		}
   111  	}
   112  
   113  	if len(relevantHooks) == 0 {
   114  		// no matching hooks
   115  		return nil
   116  	}
   117  
   118  	// Check if the request has already timed out before spawning remote calls
   119  	select {
   120  	case <-ctx.Done():
   121  		// parent context is canceled or timed out, no point in continuing
   122  		return apierrors.NewTimeoutError("request did not complete within requested timeout", 0)
   123  	default:
   124  	}
   125  
   126  	wg := sync.WaitGroup{}
   127  	errCh := make(chan error, 2*len(relevantHooks)) // double the length to handle extra errors for panics in the gofunc
   128  	wg.Add(len(relevantHooks))
   129  	for i := range relevantHooks {
   130  		go func(invocation *generic.WebhookInvocation, idx int) {
   131  			ignoreClientCallFailures := false
   132  			hookName := "unknown"
   133  			versionedAttr := versionedAttrAccessor.versionedAttrs[invocation.Kind]
   134  			// The ordering of these two defers is critical. The wg.Done will release the parent go func to close the errCh
   135  			// that is used by the second defer to report errors. The recovery and error reporting must be done first.
   136  			defer wg.Done()
   137  			defer func() {
   138  				// HandleCrash has already called the crash handlers and it has been configured to utilruntime.ReallyCrash
   139  				// This block prevents the second panic from failing our process.
   140  				// This failure mode for the handler functions properly using the channel below.
   141  				recover()
   142  			}()
   143  			defer utilruntime.HandleCrash(
   144  				func(r interface{}) {
   145  					if r == nil {
   146  						return
   147  					}
   148  					if ignoreClientCallFailures {
   149  						// if failures are supposed to ignored, ignore it
   150  						klog.Warningf("Panic calling webhook, failing open %v: %v", hookName, r)
   151  						admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hookName, "validating")
   152  						key := fmt.Sprintf("%sround_0_index_%d", ValidatingAuditAnnotationFailedOpenKeyPrefix, idx)
   153  						value := hookName
   154  						if err := versionedAttr.Attributes.AddAnnotation(key, value); err != nil {
   155  							klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, value, hookName, err)
   156  						}
   157  						return
   158  					}
   159  					// this ensures that the admission request fails and a message is provided.
   160  					errCh <- apierrors.NewInternalError(fmt.Errorf("ValidatingAdmissionWebhook/%v has panicked: %v", hookName, r))
   161  				},
   162  			)
   163  
   164  			hook, ok := invocation.Webhook.GetValidatingWebhook()
   165  			if !ok {
   166  				utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1.ValidatingWebhook, but got %T", hook))
   167  				return
   168  			}
   169  			hookName = hook.Name
   170  			ignoreClientCallFailures = hook.FailurePolicy != nil && *hook.FailurePolicy == v1.Ignore
   171  			t := time.Now()
   172  			err := d.callHook(ctx, hook, invocation, versionedAttr)
   173  			rejected := false
   174  			if err != nil {
   175  				switch err := err.(type) {
   176  				case *webhookutil.ErrCallingWebhook:
   177  					if !ignoreClientCallFailures {
   178  						rejected = true
   179  						// Ignore context cancelled from webhook metrics
   180  						if !errors.Is(err.Reason, context.Canceled) {
   181  							admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, int(err.Status.ErrStatus.Code))
   182  						}
   183  					}
   184  					admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", int(err.Status.ErrStatus.Code))
   185  				case *webhookutil.ErrWebhookRejection:
   186  					rejected = true
   187  					admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code))
   188  					admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", int(err.Status.ErrStatus.Code))
   189  				default:
   190  					rejected = true
   191  					admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0)
   192  					admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", 0)
   193  				}
   194  			} else {
   195  				admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", 200)
   196  				return
   197  			}
   198  
   199  			if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
   200  				if ignoreClientCallFailures {
   201  					// Ignore context cancelled from webhook metrics
   202  					if errors.Is(callErr.Reason, context.Canceled) {
   203  						klog.Warningf("Context canceled when calling webhook %v", hook.Name)
   204  					} else {
   205  						klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
   206  						admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hook.Name, "validating")
   207  						key := fmt.Sprintf("%sround_0_index_%d", ValidatingAuditAnnotationFailedOpenKeyPrefix, idx)
   208  						value := hook.Name
   209  						if err := versionedAttr.Attributes.AddAnnotation(key, value); err != nil {
   210  							klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, value, hook.Name, err)
   211  						}
   212  					}
   213  					utilruntime.HandleError(callErr)
   214  					return
   215  				}
   216  
   217  				klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
   218  				errCh <- apierrors.NewInternalError(err)
   219  				return
   220  			}
   221  
   222  			if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok {
   223  				err = rejectionErr.Status
   224  			}
   225  			klog.Warningf("rejected by webhook %q: %#v", hook.Name, err)
   226  			errCh <- err
   227  		}(relevantHooks[i], i)
   228  	}
   229  	wg.Wait()
   230  	close(errCh)
   231  
   232  	var errs []error
   233  	for e := range errCh {
   234  		errs = append(errs, e)
   235  	}
   236  	if len(errs) == 0 {
   237  		return nil
   238  	}
   239  	if len(errs) > 1 {
   240  		for i := 1; i < len(errs); i++ {
   241  			// TODO: merge status errors; until then, just return the first one.
   242  			utilruntime.HandleError(errs[i])
   243  		}
   244  	}
   245  	return errs[0]
   246  }
   247  
   248  func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *admission.VersionedAttributes) error {
   249  	if attr.Attributes.IsDryRun() {
   250  		if h.SideEffects == nil {
   251  			return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil"), Status: apierrors.NewBadRequest("Webhook SideEffects is nil")}
   252  		}
   253  		if !(*h.SideEffects == v1.SideEffectClassNone || *h.SideEffects == v1.SideEffectClassNoneOnDryRun) {
   254  			return webhookerrors.NewDryRunUnsupportedErr(h.Name)
   255  		}
   256  	}
   257  
   258  	uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
   259  	if err != nil {
   260  		return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not create admission objects: %w", err), Status: apierrors.NewBadRequest("error creating admission objects")}
   261  	}
   262  	// Make the webhook request
   263  	client, err := invocation.Webhook.GetRESTClient(d.cm)
   264  	if err != nil {
   265  		return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewBadRequest("error getting REST client")}
   266  	}
   267  	ctx, span := tracing.Start(ctx, "Call validating webhook",
   268  		attribute.String("configuration", invocation.Webhook.GetConfigurationName()),
   269  		attribute.String("webhook", h.Name),
   270  		attribute.Stringer("resource", attr.GetResource()),
   271  		attribute.String("subresource", attr.GetSubresource()),
   272  		attribute.String("operation", string(attr.GetOperation())),
   273  		attribute.String("UID", string(uid)))
   274  	defer span.End(500 * time.Millisecond)
   275  
   276  	// if the webhook has a specific timeout, wrap the context to apply it
   277  	if h.TimeoutSeconds != nil {
   278  		var cancel context.CancelFunc
   279  		ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second)
   280  		defer cancel()
   281  	}
   282  
   283  	r := client.Post().Body(request)
   284  
   285  	// if the context has a deadline, set it as a parameter to inform the backend
   286  	if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
   287  		// compute the timeout
   288  		if timeout := time.Until(deadline); timeout > 0 {
   289  			// if it's not an even number of seconds, round up to the nearest second
   290  			if truncated := timeout.Truncate(time.Second); truncated != timeout {
   291  				timeout = truncated + time.Second
   292  			}
   293  			// set the timeout
   294  			r.Timeout(timeout)
   295  		}
   296  	}
   297  
   298  	do := func() { err = r.Do(ctx).Into(response) }
   299  	if wd, ok := endpointsrequest.LatencyTrackersFrom(ctx); ok {
   300  		tmp := do
   301  		do = func() { wd.ValidatingWebhookTracker.Track(tmp) }
   302  	}
   303  	do()
   304  	if err != nil {
   305  		var status *apierrors.StatusError
   306  		if se, ok := err.(*apierrors.StatusError); ok {
   307  			status = se
   308  		} else {
   309  			status = apierrors.NewBadRequest("error calling webhook")
   310  		}
   311  		return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("failed to call webhook: %w", err), Status: status}
   312  	}
   313  	span.AddEvent("Request completed")
   314  
   315  	result, err := webhookrequest.VerifyAdmissionResponse(uid, false, response)
   316  	if err != nil {
   317  		return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("received invalid webhook response: %w", err), Status: apierrors.NewServiceUnavailable("error validating webhook response")}
   318  	}
   319  
   320  	for k, v := range result.AuditAnnotations {
   321  		key := h.Name + "/" + k
   322  		if err := attr.Attributes.AddAnnotation(key, v); err != nil {
   323  			klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, v, h.Name, err)
   324  		}
   325  	}
   326  	for _, w := range result.Warnings {
   327  		warning.AddWarning(ctx, "", w)
   328  	}
   329  	if result.Allowed {
   330  		return nil
   331  	}
   332  	return &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)}
   333  }