/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package generic import ( "context" "fmt" "io" admissionv1beta1 "k8s.io/api/admission/v1beta1" "k8s.io/api/admissionregistration/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/admission" genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/plugin/webhook/config" "k8s.io/apiserver/pkg/admission/plugin/webhook/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/rules" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/informers" clientset "k8s.io/client-go/kubernetes" ) // Webhook is an abstract admission plugin with all the infrastructure to define Admit or Validate on-top. type Webhook struct { *admission.Handler sourceFactory sourceFactory hookSource Source clientManager *webhook.ClientManager convertor *convertor namespaceMatcher *namespace.Matcher dispatcher Dispatcher } var ( _ genericadmissioninit.WantsExternalKubeClientSet = &Webhook{} _ admission.Interface = &Webhook{} ) type sourceFactory func(f informers.SharedInformerFactory) Source type dispatcherFactory func(cm *webhook.ClientManager) Dispatcher // NewWebhook creates a new generic admission webhook. func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) { kubeconfigFile, err := config.LoadConfig(configFile) if err != nil { return nil, err } cm, err := webhook.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme) if err != nil { return nil, err } authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver(kubeconfigFile) if err != nil { return nil, err } // Set defaults which may be overridden later. cm.SetAuthenticationInfoResolver(authInfoResolver) cm.SetServiceResolver(webhook.NewDefaultServiceResolver()) return &Webhook{ Handler: handler, sourceFactory: sourceFactory, clientManager: &cm, convertor: &convertor{}, namespaceMatcher: &namespace.Matcher{}, dispatcher: dispatcherFactory(&cm), }, nil } // SetAuthenticationInfoResolverWrapper sets the // AuthenticationInfoResolverWrapper. // TODO find a better way wire this, but keep this pull small for now. func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhook.AuthenticationInfoResolverWrapper) { a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper) } // SetServiceResolver sets a service resolver for the webhook admission plugin. // Passing a nil resolver does not have an effect, instead a default one will be used. func (a *Webhook) SetServiceResolver(sr webhook.ServiceResolver) { a.clientManager.SetServiceResolver(sr) } // SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme func (a *Webhook) SetScheme(scheme *runtime.Scheme) { if scheme != nil { a.convertor.Scheme = scheme } } // SetExternalKubeClientSet implements the WantsExternalKubeInformerFactory interface. // It sets external ClientSet for admission plugins that need it func (a *Webhook) SetExternalKubeClientSet(client clientset.Interface) { a.namespaceMatcher.Client = client } // SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface. func (a *Webhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { namespaceInformer := f.Core().V1().Namespaces() a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister() a.hookSource = a.sourceFactory(f) a.SetReadyFunc(func() bool { return namespaceInformer.Informer().HasSynced() && a.hookSource.HasSynced() }) } // ValidateInitialization implements the InitializationValidator interface. func (a *Webhook) ValidateInitialization() error { if a.hookSource == nil { return fmt.Errorf("kubernetes client is not properly setup") } if err := a.namespaceMatcher.Validate(); err != nil { return fmt.Errorf("namespaceMatcher is not properly setup: %v", err) } if err := a.clientManager.Validate(); err != nil { return fmt.Errorf("clientManager is not properly setup: %v", err) } if err := a.convertor.Validate(); err != nil { return fmt.Errorf("convertor is not properly setup: %v", err) } return nil } // ShouldCallHook makes a decision on whether to call the webhook or not by the attribute. func (a *Webhook) ShouldCallHook(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) { var matches bool for _, r := range h.Rules { m := rules.Matcher{Rule: r, Attr: attr} if m.Matches() { matches = true break } } if !matches { return false, nil } return a.namespaceMatcher.MatchNamespaceSelector(h, attr) } // Dispatch is called by the downstream Validate or Admit methods. func (a *Webhook) Dispatch(attr admission.Attributes) error { if rules.IsWebhookConfigurationResource(attr) { return nil } if !a.WaitForReady() { return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request")) } hooks := a.hookSource.Webhooks() ctx := context.TODO() var relevantHooks []*v1beta1.Webhook for i := range hooks { call, err := a.ShouldCallHook(&hooks[i], attr) if err != nil { return err } if call { relevantHooks = append(relevantHooks, &hooks[i]) } } if len(relevantHooks) == 0 { // no matching hooks return nil } // convert the object to the external version before sending it to the webhook versionedAttr := VersionedAttributes{ Attributes: attr, } if oldObj := attr.GetOldObject(); oldObj != nil { out, err := a.convertor.ConvertToGVK(oldObj, attr.GetKind()) if err != nil { return apierrors.NewInternalError(err) } versionedAttr.VersionedOldObject = out } if obj := attr.GetObject(); obj != nil { out, err := a.convertor.ConvertToGVK(obj, attr.GetKind()) if err != nil { return apierrors.NewInternalError(err) } versionedAttr.VersionedObject = out } return a.dispatcher.Dispatch(ctx, &versionedAttr, relevantHooks) }