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 }