k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/mutating/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 mutating delegates admission checks to dynamically configured 18 // mutating webhooks. 19 package mutating 20 21 import ( 22 "context" 23 "errors" 24 "fmt" 25 "time" 26 27 "go.opentelemetry.io/otel/attribute" 28 jsonpatch "gopkg.in/evanphx/json-patch.v4" 29 30 admissionv1 "k8s.io/api/admission/v1" 31 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 32 apiequality "k8s.io/apimachinery/pkg/api/equality" 33 apierrors "k8s.io/apimachinery/pkg/api/errors" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/runtime/schema" 37 "k8s.io/apimachinery/pkg/runtime/serializer/json" 38 utiljson "k8s.io/apimachinery/pkg/util/json" 39 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 40 "k8s.io/apiserver/pkg/admission" 41 admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" 42 "k8s.io/apiserver/pkg/admission/plugin/webhook" 43 webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors" 44 "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" 45 webhookrequest "k8s.io/apiserver/pkg/admission/plugin/webhook/request" 46 auditinternal "k8s.io/apiserver/pkg/apis/audit" 47 endpointsrequest "k8s.io/apiserver/pkg/endpoints/request" 48 webhookutil "k8s.io/apiserver/pkg/util/webhook" 49 "k8s.io/apiserver/pkg/warning" 50 "k8s.io/component-base/tracing" 51 "k8s.io/klog/v2" 52 ) 53 54 const ( 55 // PatchAuditAnnotationPrefix is a prefix for persisting webhook patch in audit annotation. 56 // Audit handler decides whether annotation with this prefix should be logged based on audit level. 57 // Since mutating webhook patches the request body, audit level must be greater or equal to Request 58 // for the annotation to be logged 59 PatchAuditAnnotationPrefix = "patch.webhook.admission.k8s.io/" 60 // MutationAuditAnnotationPrefix is a prefix for presisting webhook mutation existence in audit annotation. 61 MutationAuditAnnotationPrefix = "mutation.webhook.admission.k8s.io/" 62 // MutationAnnotationFailedOpenKeyPrefix in an annotation indicates 63 // the mutating webhook failed open when the webhook backend connection 64 // failed or returned an internal server error. 65 MutationAuditAnnotationFailedOpenKeyPrefix string = "failed-open." + MutationAuditAnnotationPrefix 66 ) 67 68 type mutatingDispatcher struct { 69 cm *webhookutil.ClientManager 70 plugin *Plugin 71 } 72 73 func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher { 74 return func(cm *webhookutil.ClientManager) generic.Dispatcher { 75 return &mutatingDispatcher{cm, p} 76 } 77 } 78 79 var _ generic.VersionedAttributeAccessor = &versionedAttributeAccessor{} 80 81 type versionedAttributeAccessor struct { 82 versionedAttr *admission.VersionedAttributes 83 attr admission.Attributes 84 objectInterfaces admission.ObjectInterfaces 85 } 86 87 func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) { 88 if v.versionedAttr == nil { 89 // First call, create versioned attributes 90 var err error 91 if v.versionedAttr, err = admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces); err != nil { 92 return nil, apierrors.NewInternalError(err) 93 } 94 } else { 95 // Subsequent call, convert existing versioned attributes to the requested version 96 if err := admission.ConvertVersionedAttributes(v.versionedAttr, gvk, v.objectInterfaces); err != nil { 97 return nil, apierrors.NewInternalError(err) 98 } 99 } 100 return v.versionedAttr, nil 101 } 102 103 var _ generic.Dispatcher = &mutatingDispatcher{} 104 105 func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error { 106 reinvokeCtx := attr.GetReinvocationContext() 107 var webhookReinvokeCtx *webhookReinvokeContext 108 if v := reinvokeCtx.Value(PluginName); v != nil { 109 webhookReinvokeCtx = v.(*webhookReinvokeContext) 110 } else { 111 webhookReinvokeCtx = &webhookReinvokeContext{} 112 reinvokeCtx.SetValue(PluginName, webhookReinvokeCtx) 113 } 114 115 if reinvokeCtx.IsReinvoke() && webhookReinvokeCtx.IsOutputChangedSinceLastWebhookInvocation(attr.GetObject()) { 116 // If the object has changed, we know the in-tree plugin re-invocations have mutated the object, 117 // and we need to reinvoke all eligible webhooks. 118 webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins() 119 } 120 defer func() { 121 webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject()) 122 }() 123 v := &versionedAttributeAccessor{ 124 attr: attr, 125 objectInterfaces: o, 126 } 127 for i, hook := range hooks { 128 attrForCheck := attr 129 if v.versionedAttr != nil { 130 attrForCheck = v.versionedAttr 131 } 132 133 invocation, statusErr := a.plugin.ShouldCallHook(ctx, hook, attrForCheck, o, v) 134 if statusErr != nil { 135 return statusErr 136 } 137 if invocation == nil { 138 continue 139 } 140 141 hook, ok := invocation.Webhook.GetMutatingWebhook() 142 if !ok { 143 return fmt.Errorf("mutating webhook dispatch requires v1.MutatingWebhook, but got %T", hook) 144 } 145 // This means that during reinvocation, a webhook will not be 146 // called for the first time. For example, if the webhook is 147 // skipped in the first round because of mismatching labels, 148 // even if the labels become matching, the webhook does not 149 // get called during reinvocation. 150 if reinvokeCtx.IsReinvoke() && !webhookReinvokeCtx.ShouldReinvokeWebhook(invocation.Webhook.GetUID()) { 151 continue 152 } 153 154 versionedAttr, err := v.VersionedAttribute(invocation.Kind) 155 if err != nil { 156 return apierrors.NewInternalError(err) 157 } 158 159 t := time.Now() 160 round := 0 161 if reinvokeCtx.IsReinvoke() { 162 round = 1 163 } 164 165 annotator := newWebhookAnnotator(versionedAttr, round, i, hook.Name, invocation.Webhook.GetConfigurationName()) 166 changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, annotator, o, round, i) 167 ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == admissionregistrationv1.Ignore 168 rejected := false 169 if err != nil { 170 switch err := err.(type) { 171 case *webhookutil.ErrCallingWebhook: 172 if !ignoreClientCallFailures { 173 rejected = true 174 // Ignore context cancelled from webhook metrics 175 if !errors.Is(err.Reason, context.Canceled) { 176 admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "admit", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, int(err.Status.ErrStatus.Code)) 177 } 178 } 179 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", int(err.Status.ErrStatus.Code)) 180 case *webhookutil.ErrWebhookRejection: 181 rejected = true 182 admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "admit", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code)) 183 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", int(err.Status.ErrStatus.Code)) 184 default: 185 rejected = true 186 admissionmetrics.Metrics.ObserveWebhookRejection(ctx, hook.Name, "admit", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0) 187 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", 0) 188 } 189 } else { 190 admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", 200) 191 } 192 if changed { 193 // Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation. 194 webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins() 195 reinvokeCtx.SetShouldReinvoke() 196 } 197 if hook.ReinvocationPolicy != nil && *hook.ReinvocationPolicy == admissionregistrationv1.IfNeededReinvocationPolicy { 198 webhookReinvokeCtx.AddReinvocableWebhookToPreviouslyInvoked(invocation.Webhook.GetUID()) 199 } 200 if err == nil { 201 continue 202 } 203 204 if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok { 205 if ignoreClientCallFailures { 206 // Ignore context cancelled from webhook metrics 207 if errors.Is(callErr.Reason, context.Canceled) { 208 klog.Warningf("Context canceled when calling webhook %v", hook.Name) 209 } else { 210 klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr) 211 admissionmetrics.Metrics.ObserveWebhookFailOpen(ctx, hook.Name, "admit") 212 annotator.addFailedOpenAnnotation() 213 } 214 utilruntime.HandleError(callErr) 215 216 select { 217 case <-ctx.Done(): 218 // parent context is canceled or timed out, no point in continuing 219 return apierrors.NewTimeoutError("request did not complete within requested timeout", 0) 220 default: 221 // individual webhook timed out, but parent context did not, continue 222 continue 223 } 224 } 225 klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err) 226 return apierrors.NewInternalError(err) 227 } 228 if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok { 229 return rejectionErr.Status 230 } 231 return err 232 } 233 234 // convert versionedAttr.VersionedObject to the internal version in the underlying admission.Attributes 235 if v.versionedAttr != nil && v.versionedAttr.VersionedObject != nil && v.versionedAttr.Dirty { 236 return o.GetObjectConvertor().Convert(v.versionedAttr.VersionedObject, v.versionedAttr.Attributes.GetObject(), nil) 237 } 238 239 return nil 240 } 241 242 // note that callAttrMutatingHook updates attr 243 244 func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admissionregistrationv1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *admission.VersionedAttributes, annotator *webhookAnnotator, o admission.ObjectInterfaces, round, idx int) (bool, error) { 245 configurationName := invocation.Webhook.GetConfigurationName() 246 changed := false 247 defer func() { annotator.addMutationAnnotation(changed) }() 248 if attr.Attributes.IsDryRun() { 249 if h.SideEffects == nil { 250 return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil"), Status: apierrors.NewBadRequest("Webhook SideEffects is nil")} 251 } 252 if !(*h.SideEffects == admissionregistrationv1.SideEffectClassNone || *h.SideEffects == admissionregistrationv1.SideEffectClassNoneOnDryRun) { 253 return false, webhookerrors.NewDryRunUnsupportedErr(h.Name) 254 } 255 } 256 257 uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation) 258 if err != nil { 259 return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not create admission objects: %w", err), Status: apierrors.NewBadRequest("error creating admission objects")} 260 } 261 // Make the webhook request 262 client, err := invocation.Webhook.GetRESTClient(a.cm) 263 if err != nil { 264 return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewBadRequest("error getting REST client")} 265 } 266 ctx, span := tracing.Start(ctx, "Call mutating webhook", 267 attribute.String("configuration", configurationName), 268 attribute.String("webhook", h.Name), 269 attribute.Stringer("resource", attr.GetResource()), 270 attribute.String("subresource", attr.GetSubresource()), 271 attribute.String("operation", string(attr.GetOperation())), 272 attribute.String("UID", string(uid))) 273 defer span.End(500 * time.Millisecond) 274 275 // if the webhook has a specific timeout, wrap the context to apply it 276 if h.TimeoutSeconds != nil { 277 var cancel context.CancelFunc 278 ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second) 279 defer cancel() 280 } 281 282 r := client.Post().Body(request) 283 284 // if the context has a deadline, set it as a parameter to inform the backend 285 if deadline, hasDeadline := ctx.Deadline(); hasDeadline { 286 // compute the timeout 287 if timeout := time.Until(deadline); timeout > 0 { 288 // if it's not an even number of seconds, round up to the nearest second 289 if truncated := timeout.Truncate(time.Second); truncated != timeout { 290 timeout = truncated + time.Second 291 } 292 // set the timeout 293 r.Timeout(timeout) 294 } 295 } 296 297 do := func() { err = r.Do(ctx).Into(response) } 298 if wd, ok := endpointsrequest.LatencyTrackersFrom(ctx); ok { 299 tmp := do 300 do = func() { wd.MutatingWebhookTracker.Track(tmp) } 301 } 302 do() 303 if err != nil { 304 var status *apierrors.StatusError 305 if se, ok := err.(*apierrors.StatusError); ok { 306 status = se 307 } else { 308 status = apierrors.NewBadRequest("error calling webhook") 309 } 310 return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("failed to call webhook: %w", err), Status: status} 311 } 312 span.AddEvent("Request completed") 313 314 result, err := webhookrequest.VerifyAdmissionResponse(uid, true, response) 315 if err != nil { 316 return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("received invalid webhook response: %w", err), Status: apierrors.NewServiceUnavailable("error validating webhook response")} 317 } 318 319 for k, v := range result.AuditAnnotations { 320 key := h.Name + "/" + k 321 if err := attr.Attributes.AddAnnotation(key, v); err != nil { 322 klog.Warningf("Failed to set admission audit annotation %s to %s for mutating webhook %s: %v", key, v, h.Name, err) 323 } 324 } 325 for _, w := range result.Warnings { 326 warning.AddWarning(ctx, "", w) 327 } 328 329 if !result.Allowed { 330 return false, &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)} 331 } 332 333 if len(result.Patch) == 0 { 334 return false, nil 335 } 336 patchObj, err := jsonpatch.DecodePatch(result.Patch) 337 if err != nil { 338 return false, apierrors.NewInternalError(err) 339 } 340 341 if len(patchObj) == 0 { 342 return false, nil 343 } 344 345 // if a non-empty patch was provided, and we have no object we can apply it to (e.g. a DELETE admission operation), error 346 if attr.VersionedObject == nil { 347 return false, apierrors.NewInternalError(fmt.Errorf("admission webhook %q attempted to modify the object, which is not supported for this operation", h.Name)) 348 } 349 350 var patchedJS []byte 351 jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false) 352 switch result.PatchType { 353 // VerifyAdmissionResponse normalizes to v1 patch types, regardless of the AdmissionReview version used 354 case admissionv1.PatchTypeJSONPatch: 355 objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject) 356 if err != nil { 357 return false, apierrors.NewInternalError(err) 358 } 359 patchedJS, err = patchObj.Apply(objJS) 360 if err != nil { 361 return false, apierrors.NewInternalError(err) 362 } 363 default: 364 return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("unsupported patch type %q", result.PatchType), Status: webhookerrors.ToStatusErr(h.Name, result.Result)} 365 } 366 367 var newVersionedObject runtime.Object 368 if _, ok := attr.VersionedObject.(*unstructured.Unstructured); ok { 369 // Custom Resources don't have corresponding Go struct's. 370 // They are represented as Unstructured. 371 newVersionedObject = &unstructured.Unstructured{} 372 } else { 373 newVersionedObject, err = o.GetObjectCreater().New(attr.VersionedKind) 374 if err != nil { 375 return false, apierrors.NewInternalError(err) 376 } 377 } 378 379 // TODO: if we have multiple mutating webhooks, we can remember the json 380 // instead of encoding and decoding for each one. 381 if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil { 382 return false, apierrors.NewInternalError(err) 383 } 384 385 changed = !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject) 386 span.AddEvent("Patch applied") 387 annotator.addPatchAnnotation(patchObj, result.PatchType) 388 attr.Dirty = true 389 attr.VersionedObject = newVersionedObject 390 o.GetObjectDefaulter().Default(attr.VersionedObject) 391 return changed, nil 392 } 393 394 type webhookAnnotator struct { 395 attr *admission.VersionedAttributes 396 failedOpenAnnotationKey string 397 patchAnnotationKey string 398 mutationAnnotationKey string 399 webhook string 400 configuration string 401 } 402 403 func newWebhookAnnotator(attr *admission.VersionedAttributes, round, idx int, webhook, configuration string) *webhookAnnotator { 404 return &webhookAnnotator{ 405 attr: attr, 406 failedOpenAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", MutationAuditAnnotationFailedOpenKeyPrefix, round, idx), 407 patchAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", PatchAuditAnnotationPrefix, round, idx), 408 mutationAnnotationKey: fmt.Sprintf("%sround_%d_index_%d", MutationAuditAnnotationPrefix, round, idx), 409 webhook: webhook, 410 configuration: configuration, 411 } 412 } 413 414 func (w *webhookAnnotator) addFailedOpenAnnotation() { 415 if w.attr == nil || w.attr.Attributes == nil { 416 return 417 } 418 value := w.webhook 419 if err := w.attr.Attributes.AddAnnotation(w.failedOpenAnnotationKey, value); err != nil { 420 klog.Warningf("failed to set failed open annotation for mutating webhook key %s to %s: %v", w.failedOpenAnnotationKey, value, err) 421 } 422 } 423 424 func (w *webhookAnnotator) addMutationAnnotation(mutated bool) { 425 if w.attr == nil || w.attr.Attributes == nil { 426 return 427 } 428 value, err := mutationAnnotationValue(w.configuration, w.webhook, mutated) 429 if err != nil { 430 klog.Warningf("unexpected error composing mutating webhook annotation: %v", err) 431 return 432 } 433 if err := w.attr.Attributes.AddAnnotation(w.mutationAnnotationKey, value); err != nil { 434 klog.Warningf("failed to set mutation annotation for mutating webhook key %s to %s: %v", w.mutationAnnotationKey, value, err) 435 } 436 } 437 438 func (w *webhookAnnotator) addPatchAnnotation(patch interface{}, patchType admissionv1.PatchType) { 439 if w.attr == nil || w.attr.Attributes == nil { 440 return 441 } 442 var value string 443 var err error 444 switch patchType { 445 case admissionv1.PatchTypeJSONPatch: 446 value, err = jsonPatchAnnotationValue(w.configuration, w.webhook, patch) 447 if err != nil { 448 klog.Warningf("unexpected error composing mutating webhook JSON patch annotation: %v", err) 449 return 450 } 451 default: 452 klog.Warningf("unsupported patch type for mutating webhook annotation: %v", patchType) 453 return 454 } 455 if err := w.attr.Attributes.AddAnnotationWithLevel(w.patchAnnotationKey, value, auditinternal.LevelRequest); err != nil { 456 // NOTE: we don't log actual patch in kube-apiserver log to avoid potentially 457 // leaking information 458 klog.Warningf("failed to set patch annotation for mutating webhook key %s; confugiration name: %s, webhook name: %s", w.patchAnnotationKey, w.configuration, w.webhook) 459 } 460 } 461 462 // MutationAuditAnnotation logs if a webhook invocation mutated the request object 463 type MutationAuditAnnotation struct { 464 Configuration string `json:"configuration"` 465 Webhook string `json:"webhook"` 466 Mutated bool `json:"mutated"` 467 } 468 469 // PatchAuditAnnotation logs a patch from a mutating webhook 470 type PatchAuditAnnotation struct { 471 Configuration string `json:"configuration"` 472 Webhook string `json:"webhook"` 473 Patch interface{} `json:"patch,omitempty"` 474 PatchType string `json:"patchType,omitempty"` 475 } 476 477 func mutationAnnotationValue(configuration, webhook string, mutated bool) (string, error) { 478 m := MutationAuditAnnotation{ 479 Configuration: configuration, 480 Webhook: webhook, 481 Mutated: mutated, 482 } 483 bytes, err := utiljson.Marshal(m) 484 return string(bytes), err 485 } 486 487 func jsonPatchAnnotationValue(configuration, webhook string, patch interface{}) (string, error) { 488 p := PatchAuditAnnotation{ 489 Configuration: configuration, 490 Webhook: webhook, 491 Patch: patch, 492 PatchType: string(admissionv1.PatchTypeJSONPatch), 493 } 494 bytes, err := utiljson.Marshal(p) 495 return string(bytes), err 496 }