k8s.io/apiserver@v0.31.1/pkg/admission/plugin/policy/generic/plugin.go (about)

     1  /*
     2  Copyright 2024 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  	"errors"
    22  	"fmt"
    23  
    24  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/apiserver/pkg/admission"
    30  	"k8s.io/apiserver/pkg/admission/initializer"
    31  	"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
    32  	"k8s.io/apiserver/pkg/authorization/authorizer"
    33  	"k8s.io/client-go/dynamic"
    34  	"k8s.io/client-go/informers"
    35  	"k8s.io/client-go/kubernetes"
    36  )
    37  
    38  // H is the Hook type generated by the source and consumed by the dispatcher.
    39  type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
    40  type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
    41  
    42  // admissionResources is the list of resources related to CEL-based admission
    43  // features.
    44  var admissionResources = []schema.GroupResource{
    45  	{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicies"},
    46  	{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicybindings"},
    47  	{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicies"},
    48  	{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicybindings"},
    49  }
    50  
    51  // AdmissionPolicyManager is an abstract admission plugin with all the
    52  // infrastructure to define Admit or Validate on-top.
    53  type Plugin[H any] struct {
    54  	*admission.Handler
    55  
    56  	sourceFactory     sourceFactory[H]
    57  	dispatcherFactory dispatcherFactory[H]
    58  
    59  	source     Source[H]
    60  	dispatcher Dispatcher[H]
    61  	matcher    *matching.Matcher
    62  
    63  	informerFactory   informers.SharedInformerFactory
    64  	client            kubernetes.Interface
    65  	restMapper        meta.RESTMapper
    66  	dynamicClient     dynamic.Interface
    67  	excludedResources sets.Set[schema.GroupResource]
    68  	stopCh            <-chan struct{}
    69  	authorizer        authorizer.Authorizer
    70  	enabled           bool
    71  }
    72  
    73  var (
    74  	_ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{}
    75  	_ initializer.WantsExternalKubeClientSet       = &Plugin[any]{}
    76  	_ initializer.WantsRESTMapper                  = &Plugin[any]{}
    77  	_ initializer.WantsDynamicClient               = &Plugin[any]{}
    78  	_ initializer.WantsDrainedNotification         = &Plugin[any]{}
    79  	_ initializer.WantsAuthorizer                  = &Plugin[any]{}
    80  	_ initializer.WantsExcludedAdmissionResources  = &Plugin[any]{}
    81  	_ admission.InitializationValidator            = &Plugin[any]{}
    82  )
    83  
    84  func NewPlugin[H any](
    85  	handler *admission.Handler,
    86  	sourceFactory sourceFactory[H],
    87  	dispatcherFactory dispatcherFactory[H],
    88  ) *Plugin[H] {
    89  	return &Plugin[H]{
    90  		Handler:           handler,
    91  		sourceFactory:     sourceFactory,
    92  		dispatcherFactory: dispatcherFactory,
    93  
    94  		// always exclude admission/mutating policies and bindings
    95  		excludedResources: sets.New(admissionResources...),
    96  	}
    97  }
    98  
    99  func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
   100  	c.informerFactory = f
   101  }
   102  
   103  func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) {
   104  	c.client = client
   105  }
   106  
   107  func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) {
   108  	c.restMapper = mapper
   109  }
   110  
   111  func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) {
   112  	c.dynamicClient = client
   113  }
   114  
   115  func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) {
   116  	c.stopCh = stopCh
   117  }
   118  
   119  func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) {
   120  	c.authorizer = authorizer
   121  }
   122  
   123  func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) {
   124  	c.matcher = matcher
   125  }
   126  
   127  func (c *Plugin[H]) SetEnabled(enabled bool) {
   128  	c.enabled = enabled
   129  }
   130  
   131  func (c *Plugin[H]) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) {
   132  	c.excludedResources.Insert(excludedResources...)
   133  }
   134  
   135  // ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
   136  func (c *Plugin[H]) ValidateInitialization() error {
   137  	// By default enabled is set to false. It is up to types which embed this
   138  	// struct to set it to true (if feature gate is enabled, or other conditions)
   139  	if !c.enabled {
   140  		return nil
   141  	}
   142  	if c.Handler == nil {
   143  		return errors.New("missing handler")
   144  	}
   145  	if c.informerFactory == nil {
   146  		return errors.New("missing informer factory")
   147  	}
   148  	if c.client == nil {
   149  		return errors.New("missing kubernetes client")
   150  	}
   151  	if c.restMapper == nil {
   152  		return errors.New("missing rest mapper")
   153  	}
   154  	if c.dynamicClient == nil {
   155  		return errors.New("missing dynamic client")
   156  	}
   157  	if c.stopCh == nil {
   158  		return errors.New("missing stop channel")
   159  	}
   160  	if c.authorizer == nil {
   161  		return errors.New("missing authorizer")
   162  	}
   163  
   164  	// Use default matcher
   165  	namespaceInformer := c.informerFactory.Core().V1().Namespaces()
   166  	c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client)
   167  
   168  	if err := c.matcher.ValidateInitialization(); err != nil {
   169  		return err
   170  	}
   171  
   172  	c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
   173  	c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
   174  
   175  	pluginContext, pluginContextCancel := context.WithCancel(context.Background())
   176  	go func() {
   177  		defer pluginContextCancel()
   178  		<-c.stopCh
   179  	}()
   180  
   181  	go func() {
   182  		err := c.source.Run(pluginContext)
   183  		if err != nil && !errors.Is(err, context.Canceled) {
   184  			utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
   185  		}
   186  	}()
   187  
   188  	c.SetReadyFunc(func() bool {
   189  		return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
   190  	})
   191  	return nil
   192  }
   193  
   194  func (c *Plugin[H]) Dispatch(
   195  	ctx context.Context,
   196  	a admission.Attributes,
   197  	o admission.ObjectInterfaces,
   198  ) (err error) {
   199  	if !c.enabled {
   200  		return nil
   201  	} else if c.shouldIgnoreResource(a) {
   202  		return nil
   203  	} else if !c.WaitForReady() {
   204  		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
   205  	}
   206  
   207  	return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks())
   208  }
   209  
   210  func (c *Plugin[H]) shouldIgnoreResource(attr admission.Attributes) bool {
   211  	gvr := attr.GetResource()
   212  	// exclusion decision ignores the version.
   213  	gr := gvr.GroupResource()
   214  	return c.excludedResources.Has(gr)
   215  }