k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/validating/dispatcher.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 validating 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sync" 24 "time" 25 26 "go.opentelemetry.io/otel/attribute" 27 28 v1 "k8s.io/api/admissionregistration/v1" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 "k8s.io/apiserver/pkg/admission" 33 admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" 34 "k8s.io/apiserver/pkg/admission/plugin/webhook" 35 webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors" 36 "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" 37 webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request" 38 endpointsrequest "k8s.io/apiserver/pkg/endpoints/request" 39 webhookutil "k8s.io/apiserver/pkg/util/webhook" 40 "k8s.io/apiserver/pkg/warning" 41 "k8s.io/component-base/tracing" 42 "k8s.io/klog/v2" 43 ) 44 45 const ( 46 // ValidatingAuditAnnotationPrefix is a prefix for keeping noteworthy 47 // validating audit annotations. 48 ValidatingAuditAnnotationPrefix = "validating.webhook.admission.k8s.io/" 49 // ValidatingAuditAnnotationFailedOpenKeyPrefix in an annotation indicates 50 // the validating webhook failed open when the webhook backend connection 51 // failed or returned an internal server error. 52 ValidatingAuditAnnotationFailedOpenKeyPrefix = "failed-open." + ValidatingAuditAnnotationPrefix 53 ) 54 55 type validatingDispatcher struct { 56 cm *webhookutil.ClientManager 57 plugin *Plugin 58 } 59 60 func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher { 61 return func(cm *webhookutil.ClientManager) generic.Dispatcher { 62 return &validatingDispatcher{cm, p} 63 } 64 } 65 66 var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{} 67 68 type versionedAttributeAccessor struct { 69 versionedAttrs map[schema.GroupVersionKind]*admission.VersionedAttributes 70 attr admission.Attributes 71 objectInterfaces admission.ObjectInterfaces 72 } 73 74 func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) { 75 if val, ok := v.versionedAttrs[gvk]; ok { 76 return val, nil 77 } 78 versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces) 79 if err != nil { 80 return nil, err 81 } 82 v.versionedAttrs[gvk] = versionedAttr 83 return versionedAttr, nil 84 } 85 86 var _ generic.Dispatcher = &validatingDispatcher{} 87 88 func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error { 89 var relevantHooks []*generic.WebhookInvocation 90 // Construct all the versions we need to call our webhooks 91 versionedAttrAccessor := &versionedAttributeAccessor{ 92 versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{}, 93 attr: attr, 94 objectInterfaces: o, 95 } 96 for _, hook := range hooks { 97 invocation, statusError := d.plugin.ShouldCallHook(ctx, hook, attr, o, versionedAttrAccessor) 98 if statusError != nil { 99 return statusError 100 } 101 if invocation == nil { 102 continue 103 } 104 105 relevantHooks = append(relevantHooks, invocation) 106 // VersionedAttr result will be cached and reused later during parallel webhook calls 107 _, err := versionedAttrAccessor.VersionedAttribute(invocation.Kind) 108 if err != nil { 109 return apierrors.NewInternalError(err) 110 } 111 } 112 113 if len(relevantHooks) == 0 { 114 // no matching hooks 115 return nil 116 } 117 118 // Check if the request has already timed out before spawning remote calls 119 select { 120 case <-ctx.Done(): 121 // parent context is canceled or timed out, no point in continuing 122 return apierrors.NewTimeoutError("request did not complete within requested timeout", 0) 123 default: 124 } 125 126 wg := sync.WaitGroup{} 127 errCh := make(chan error, 2*len(relevantHooks)) // double the length to handle extra errors for panics in the gofunc 128 wg.Add(len(relevantHooks)) 129 for i := range relevantHooks { 130 go func(invocation *generic.WebhookInvocation, idx int) { 131 ignoreClientCallFailures := false 132 hookName := "unknown" 133 versionedAttr := versionedAttrAccessor.versionedAttrs[invocation.Kind] 134 // The ordering of these two defers is critical. The wg.Done will release the parent go func to close the errCh 135 // that is used by the second defer to report errors. The recovery and error reporting must be done first. 136 defer wg.Done() 137 defer func() { 138 // HandleCrash has already called the crash handlers and it has been configured to utilruntime.ReallyCrash 139 // This block prevents the second panic from failing our process. 140 // This failure mode for the handler functions properly using the channel below. 141 recover() 142 }() 143 defer utilruntime.HandleCrash( 144 func(r interface{}) { 145 if r == nil { 146 return 147 } 148 if ignoreClientCallFailures { 149 // if failures are supposed to ignored, ignore it 150 klog.Warningf("Panic calling webhook, failing open %v: %v", hookName, r) 151 admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hookName, "validating") 152 key := fmt.Sprintf("%sround_0_index_%d", ValidatingAuditAnnotationFailedOpenKeyPrefix, idx) 153 value := hookName 154 if err := versionedAttr.Attributes.AddAnnotation(key, value); err != nil { 155 klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, value, hookName, err) 156 } 157 return 158 } 159 // this ensures that the admission request fails and a message is provided. 160 errCh <- apierrors.NewInternalError(fmt.Errorf("ValidatingAdmissionWebhook/%v has panicked: %v", hookName, r)) 161 }, 162 ) 163 164 hook, ok := invocation.Webhook.GetValidatingWebhook() 165 if !ok { 166 utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1.ValidatingWebhook, but got %T", hook)) 167 return 168 } 169 hookName = hook.Name 170 ignoreClientCallFailures = hook.FailurePolicy != nil && *hook.FailurePolicy == v1.Ignore 171 t := time.Now() 172 err := d.callHook(ctx, hook, invocation, versionedAttr) 173 rejected := false 174 if err != nil { 175 switch err := err.(type) { 176 case *webhookutil.ErrCallingWebhook: 177 if !ignoreClientCallFailures { 178 rejected = true 179 // Ignore context cancelled from webhook metrics 180 if !errors.Is(err.Reason, context.Canceled) { 181 admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, int(err.Status.ErrStatus.Code)) 182 } 183 } 184 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", int(err.Status.ErrStatus.Code)) 185 case *webhookutil.ErrWebhookRejection: 186 rejected = true 187 admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code)) 188 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", int(err.Status.ErrStatus.Code)) 189 default: 190 rejected = true 191 admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0) 192 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", 0) 193 } 194 } else { 195 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "validating", 200) 196 return 197 } 198 199 if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok { 200 if ignoreClientCallFailures { 201 // Ignore context cancelled from webhook metrics 202 if errors.Is(callErr.Reason, context.Canceled) { 203 klog.Warningf("Context canceled when calling webhook %v", hook.Name) 204 } else { 205 klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) 206 admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hook.Name, "validating") 207 key := fmt.Sprintf("%sround_0_index_%d", ValidatingAuditAnnotationFailedOpenKeyPrefix, idx) 208 value := hook.Name 209 if err := versionedAttr.Attributes.AddAnnotation(key, value); err != nil { 210 klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, value, hook.Name, err) 211 } 212 } 213 utilruntime.HandleError(callErr) 214 return 215 } 216 217 klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err) 218 errCh <- apierrors.NewInternalError(err) 219 return 220 } 221 222 if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok { 223 err = rejectionErr.Status 224 } 225 klog.Warningf("rejected by webhook %q: %#v", hook.Name, err) 226 errCh <- err 227 }(relevantHooks[i], i) 228 } 229 wg.Wait() 230 close(errCh) 231 232 var errs []error 233 for e := range errCh { 234 errs = append(errs, e) 235 } 236 if len(errs) == 0 { 237 return nil 238 } 239 if len(errs) > 1 { 240 for i := 1; i < len(errs); i++ { 241 // TODO: merge status errors; until then, just return the first one. 242 utilruntime.HandleError(errs[i]) 243 } 244 } 245 return errs[0] 246 } 247 248 func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *admission.VersionedAttributes) error { 249 if attr.Attributes.IsDryRun() { 250 if h.SideEffects == nil { 251 return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil"), Status: apierrors.NewBadRequest("Webhook SideEffects is nil")} 252 } 253 if !(*h.SideEffects == v1.SideEffectClassNone || *h.SideEffects == v1.SideEffectClassNoneOnDryRun) { 254 return webhookerrors.NewDryRunUnsupportedErr(h.Name) 255 } 256 } 257 258 uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation) 259 if err != nil { 260 return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not create admission objects: %w", err), Status: apierrors.NewBadRequest("error creating admission objects")} 261 } 262 // Make the webhook request 263 client, err := invocation.Webhook.GetRESTClient(d.cm) 264 if err != nil { 265 return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewBadRequest("error getting REST client")} 266 } 267 ctx, span := tracing.Start(ctx, "Call validating webhook", 268 attribute.String("configuration", invocation.Webhook.GetConfigurationName()), 269 attribute.String("webhook", h.Name), 270 attribute.Stringer("resource", attr.GetResource()), 271 attribute.String("subresource", attr.GetSubresource()), 272 attribute.String("operation", string(attr.GetOperation())), 273 attribute.String("UID", string(uid))) 274 defer span.End(500 * time.Millisecond) 275 276 // if the webhook has a specific timeout, wrap the context to apply it 277 if h.TimeoutSeconds != nil { 278 var cancel context.CancelFunc 279 ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second) 280 defer cancel() 281 } 282 283 r := client.Post().Body(request) 284 285 // if the context has a deadline, set it as a parameter to inform the backend 286 if deadline, hasDeadline := ctx.Deadline(); hasDeadline { 287 // compute the timeout 288 if timeout := time.Until(deadline); timeout > 0 { 289 // if it's not an even number of seconds, round up to the nearest second 290 if truncated := timeout.Truncate(time.Second); truncated != timeout { 291 timeout = truncated + time.Second 292 } 293 // set the timeout 294 r.Timeout(timeout) 295 } 296 } 297 298 do := func() { err = r.Do(ctx).Into(response) } 299 if wd, ok := endpointsrequest.LatencyTrackersFrom(ctx); ok { 300 tmp := do 301 do = func() { wd.ValidatingWebhookTracker.Track(tmp) } 302 } 303 do() 304 if err != nil { 305 var status *apierrors.StatusError 306 if se, ok := err.(*apierrors.StatusError); ok { 307 status = se 308 } else { 309 status = apierrors.NewBadRequest("error calling webhook") 310 } 311 return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("failed to call webhook: %w", err), Status: status} 312 } 313 span.AddEvent("Request completed") 314 315 result, err := webhookrequest.VerifyAdmissionResponse(uid, false, response) 316 if err != nil { 317 return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("received invalid webhook response: %w", err), Status: apierrors.NewServiceUnavailable("error validating webhook response")} 318 } 319 320 for k, v := range result.AuditAnnotations { 321 key := h.Name + "/" + k 322 if err := attr.Attributes.AddAnnotation(key, v); err != nil { 323 klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, v, h.Name, err) 324 } 325 } 326 for _, w := range result.Warnings { 327 warning.AddWarning(ctx, "", w) 328 } 329 if result.Allowed { 330 return nil 331 } 332 return &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)} 333 }