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 }