k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/generic/webhook.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 generic
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  
    24  	"k8s.io/klog/v2"
    25  
    26  	admissionv1 "k8s.io/api/admission/v1"
    27  	admissionv1beta1 "k8s.io/api/admission/v1beta1"
    28  	v1 "k8s.io/api/admissionregistration/v1"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/apiserver/pkg/admission"
    32  	genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
    33  	admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
    34  	"k8s.io/apiserver/pkg/admission/plugin/cel"
    35  	"k8s.io/apiserver/pkg/admission/plugin/webhook"
    36  	"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
    37  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
    38  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
    39  	"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
    40  	"k8s.io/apiserver/pkg/authorization/authorizer"
    41  	"k8s.io/apiserver/pkg/cel/environment"
    42  	"k8s.io/apiserver/pkg/features"
    43  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    44  	webhookutil "k8s.io/apiserver/pkg/util/webhook"
    45  	"k8s.io/client-go/informers"
    46  	clientset "k8s.io/client-go/kubernetes"
    47  )
    48  
    49  // Webhook is an abstract admission plugin with all the infrastructure to define Admit or Validate on-top.
    50  type Webhook struct {
    51  	*admission.Handler
    52  
    53  	sourceFactory sourceFactory
    54  
    55  	hookSource       Source
    56  	clientManager    *webhookutil.ClientManager
    57  	namespaceMatcher *namespace.Matcher
    58  	objectMatcher    *object.Matcher
    59  	dispatcher       Dispatcher
    60  	filterCompiler   cel.FilterCompiler
    61  	authorizer       authorizer.Authorizer
    62  }
    63  
    64  var (
    65  	_ genericadmissioninit.WantsExternalKubeClientSet = &Webhook{}
    66  	_ admission.Interface                             = &Webhook{}
    67  )
    68  
    69  type sourceFactory func(f informers.SharedInformerFactory) Source
    70  type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher
    71  
    72  // NewWebhook creates a new generic admission webhook.
    73  func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) {
    74  	kubeconfigFile, err := config.LoadConfig(configFile)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	cm, err := webhookutil.NewClientManager(
    80  		[]schema.GroupVersion{
    81  			admissionv1beta1.SchemeGroupVersion,
    82  			admissionv1.SchemeGroupVersion,
    83  		},
    84  		admissionv1beta1.AddToScheme,
    85  		admissionv1.AddToScheme,
    86  	)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	// Set defaults which may be overridden later.
    95  	cm.SetAuthenticationInfoResolver(authInfoResolver)
    96  	cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver())
    97  
    98  	return &Webhook{
    99  		Handler:          handler,
   100  		sourceFactory:    sourceFactory,
   101  		clientManager:    &cm,
   102  		namespaceMatcher: &namespace.Matcher{},
   103  		objectMatcher:    &object.Matcher{},
   104  		dispatcher:       dispatcherFactory(&cm),
   105  		filterCompiler:   cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
   106  	}, nil
   107  }
   108  
   109  // SetAuthenticationInfoResolverWrapper sets the
   110  // AuthenticationInfoResolverWrapper.
   111  // TODO find a better way wire this, but keep this pull small for now.
   112  func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhookutil.AuthenticationInfoResolverWrapper) {
   113  	a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
   114  }
   115  
   116  // SetServiceResolver sets a service resolver for the webhook admission plugin.
   117  // Passing a nil resolver does not have an effect, instead a default one will be used.
   118  func (a *Webhook) SetServiceResolver(sr webhookutil.ServiceResolver) {
   119  	a.clientManager.SetServiceResolver(sr)
   120  }
   121  
   122  // SetExternalKubeClientSet implements the WantsExternalKubeInformerFactory interface.
   123  // It sets external ClientSet for admission plugins that need it
   124  func (a *Webhook) SetExternalKubeClientSet(client clientset.Interface) {
   125  	a.namespaceMatcher.Client = client
   126  }
   127  
   128  // SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
   129  func (a *Webhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
   130  	namespaceInformer := f.Core().V1().Namespaces()
   131  	a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister()
   132  	a.hookSource = a.sourceFactory(f)
   133  	a.SetReadyFunc(func() bool {
   134  		return namespaceInformer.Informer().HasSynced() && a.hookSource.HasSynced()
   135  	})
   136  }
   137  
   138  func (a *Webhook) SetAuthorizer(authorizer authorizer.Authorizer) {
   139  	a.authorizer = authorizer
   140  }
   141  
   142  // ValidateInitialization implements the InitializationValidator interface.
   143  func (a *Webhook) ValidateInitialization() error {
   144  	if a.hookSource == nil {
   145  		return fmt.Errorf("kubernetes client is not properly setup")
   146  	}
   147  	if err := a.namespaceMatcher.Validate(); err != nil {
   148  		return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
   149  	}
   150  	if err := a.clientManager.Validate(); err != nil {
   151  		return fmt.Errorf("clientManager is not properly setup: %v", err)
   152  	}
   153  	return nil
   154  }
   155  
   156  // ShouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called,
   157  // or an error if an error was encountered during evaluation.
   158  func (a *Webhook) ShouldCallHook(ctx context.Context, h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces, v VersionedAttributeAccessor) (*WebhookInvocation, *apierrors.StatusError) {
   159  	matches, matchNsErr := a.namespaceMatcher.MatchNamespaceSelector(h, attr)
   160  	// Should not return an error here for webhooks which do not apply to the request, even if err is an unexpected scenario.
   161  	if !matches && matchNsErr == nil {
   162  		return nil, nil
   163  	}
   164  
   165  	// Should not return an error here for webhooks which do not apply to the request, even if err is an unexpected scenario.
   166  	matches, matchObjErr := a.objectMatcher.MatchObjectSelector(h, attr)
   167  	if !matches && matchObjErr == nil {
   168  		return nil, nil
   169  	}
   170  
   171  	var invocation *WebhookInvocation
   172  	for _, r := range h.GetRules() {
   173  		m := rules.Matcher{Rule: r, Attr: attr}
   174  		if m.Matches() {
   175  			invocation = &WebhookInvocation{
   176  				Webhook:     h,
   177  				Resource:    attr.GetResource(),
   178  				Subresource: attr.GetSubresource(),
   179  				Kind:        attr.GetKind(),
   180  			}
   181  			break
   182  		}
   183  	}
   184  	if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1.Equivalent {
   185  		attrWithOverride := &attrWithResourceOverride{Attributes: attr}
   186  		equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
   187  		// honor earlier rules first
   188  	OuterLoop:
   189  		for _, r := range h.GetRules() {
   190  			// see if the rule matches any of the equivalent resources
   191  			for _, equivalent := range equivalents {
   192  				if equivalent == attr.GetResource() {
   193  					// exclude attr.GetResource(), which we already checked
   194  					continue
   195  				}
   196  				attrWithOverride.resource = equivalent
   197  				m := rules.Matcher{Rule: r, Attr: attrWithOverride}
   198  				if m.Matches() {
   199  					kind := o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
   200  					if kind.Empty() {
   201  						return nil, apierrors.NewInternalError(fmt.Errorf("unable to convert to %v: unknown kind", equivalent))
   202  					}
   203  					invocation = &WebhookInvocation{
   204  						Webhook:     h,
   205  						Resource:    equivalent,
   206  						Subresource: attr.GetSubresource(),
   207  						Kind:        kind,
   208  					}
   209  					break OuterLoop
   210  				}
   211  			}
   212  		}
   213  	}
   214  
   215  	if invocation == nil {
   216  		return nil, nil
   217  	}
   218  	if matchNsErr != nil {
   219  		return nil, matchNsErr
   220  	}
   221  	if matchObjErr != nil {
   222  		return nil, matchObjErr
   223  	}
   224  	matchConditions := h.GetMatchConditions()
   225  	if len(matchConditions) > 0 {
   226  		versionedAttr, err := v.VersionedAttribute(invocation.Kind)
   227  		if err != nil {
   228  			return nil, apierrors.NewInternalError(err)
   229  		}
   230  
   231  		matcher := h.GetCompiledMatcher(a.filterCompiler)
   232  		matchResult := matcher.Match(ctx, versionedAttr, nil, a.authorizer)
   233  
   234  		if matchResult.Error != nil {
   235  			klog.Warningf("Failed evaluating match conditions, failing closed %v: %v", h.GetName(), matchResult.Error)
   236  			return nil, apierrors.NewForbidden(attr.GetResource().GroupResource(), attr.GetName(), matchResult.Error)
   237  		} else if !matchResult.Matches {
   238  			admissionmetrics.Metrics.ObserveMatchConditionExclusion(ctx, h.GetName(), "webhook", h.GetType(), string(attr.GetOperation()))
   239  			// if no match, always skip webhook
   240  			return nil, nil
   241  		}
   242  	}
   243  
   244  	return invocation, nil
   245  }
   246  
   247  type attrWithResourceOverride struct {
   248  	admission.Attributes
   249  	resource schema.GroupVersionResource
   250  }
   251  
   252  func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }
   253  
   254  // Dispatch is called by the downstream Validate or Admit methods.
   255  func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
   256  	if rules.IsExemptAdmissionConfigurationResource(attr) {
   257  		return nil
   258  	}
   259  	if !a.WaitForReady() {
   260  		return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
   261  	}
   262  	hooks := a.hookSource.Webhooks()
   263  	return a.dispatcher.Dispatch(ctx, attr, o, hooks)
   264  }