knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/resourcesemantics/defaulting/defaulting.go (about) 1 /* 2 Copyright 2019 The Knative 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 defaulting 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sort" 24 "strings" 25 26 "github.com/gobuffalo/flect" 27 "go.uber.org/zap" 28 "gomodules.xyz/jsonpatch/v2" 29 30 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 31 32 admissionv1 "k8s.io/api/admission/v1" 33 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 34 corev1 "k8s.io/api/core/v1" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/apimachinery/pkg/types" 39 "k8s.io/client-go/kubernetes" 40 admissionlisters "k8s.io/client-go/listers/admissionregistration/v1" 41 corelisters "k8s.io/client-go/listers/core/v1" 42 43 "knative.dev/pkg/apis" 44 "knative.dev/pkg/apis/duck" 45 "knative.dev/pkg/controller" 46 "knative.dev/pkg/kmp" 47 "knative.dev/pkg/logging" 48 "knative.dev/pkg/ptr" 49 pkgreconciler "knative.dev/pkg/reconciler" 50 "knative.dev/pkg/system" 51 "knative.dev/pkg/webhook" 52 certresources "knative.dev/pkg/webhook/certificates/resources" 53 "knative.dev/pkg/webhook/json" 54 "knative.dev/pkg/webhook/resourcesemantics" 55 ) 56 57 var errMissingNewObject = errors.New("the new object may not be nil") 58 59 // reconciler implements the AdmissionController for resources 60 type reconciler struct { 61 webhook.StatelessAdmissionImpl 62 pkgreconciler.LeaderAwareFuncs 63 64 key types.NamespacedName 65 path string 66 handlers map[schema.GroupVersionKind]resourcesemantics.GenericCRD 67 callbacks map[schema.GroupVersionKind]Callback 68 69 withContext func(context.Context) context.Context 70 71 client kubernetes.Interface 72 mwhlister admissionlisters.MutatingWebhookConfigurationLister 73 secretlister corelisters.SecretLister 74 75 disallowUnknownFields bool 76 secretName string 77 disableNamespaceOwnership bool 78 } 79 80 // CallbackFunc is the function to be invoked. 81 type CallbackFunc func(ctx context.Context, unstructured *unstructured.Unstructured) error 82 83 // Callback is a generic function to be called by a consumer of defaulting. 84 type Callback struct { 85 // function is the callback to be invoked. 86 function CallbackFunc 87 88 // supportedVerbs are the verbs supported for the callback. 89 // The function will only be called on these actions. 90 supportedVerbs map[webhook.Operation]struct{} 91 } 92 93 // NewCallback creates a new callback function to be invoked on supported verbs. 94 func NewCallback(function func(context.Context, *unstructured.Unstructured) error, supportedVerbs ...webhook.Operation) Callback { 95 if function == nil { 96 panic("expected function, got nil") 97 } 98 m := make(map[webhook.Operation]struct{}) 99 for _, op := range supportedVerbs { 100 if op == webhook.Delete { 101 panic("Verb " + webhook.Delete + " not allowed") 102 } 103 if _, has := m[op]; has { 104 panic("duplicate verbs not allowed") 105 } 106 m[op] = struct{}{} 107 } 108 return Callback{function: function, supportedVerbs: m} 109 } 110 111 var ( 112 _ controller.Reconciler = (*reconciler)(nil) 113 _ pkgreconciler.LeaderAware = (*reconciler)(nil) 114 _ webhook.AdmissionController = (*reconciler)(nil) 115 _ webhook.StatelessAdmissionController = (*reconciler)(nil) 116 ) 117 118 // Reconcile implements controller.Reconciler 119 func (ac *reconciler) Reconcile(ctx context.Context, key string) error { 120 logger := logging.FromContext(ctx) 121 122 if !ac.IsLeaderFor(ac.key) { 123 return controller.NewSkipKey(key) 124 } 125 126 // Look up the webhook secret, and fetch the CA cert bundle. 127 secret, err := ac.secretlister.Secrets(system.Namespace()).Get(ac.secretName) 128 if err != nil { 129 logger.Errorw("Error fetching secret", zap.Error(err)) 130 return err 131 } 132 caCert, ok := secret.Data[certresources.CACert] 133 if !ok { 134 return fmt.Errorf("secret %q is missing %q key", ac.secretName, certresources.CACert) 135 } 136 137 // Reconcile the webhook configuration. 138 return ac.reconcileMutatingWebhook(ctx, caCert) 139 } 140 141 // Path implements AdmissionController 142 func (ac *reconciler) Path() string { 143 return ac.path 144 } 145 146 // Admit implements AdmissionController 147 func (ac *reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 148 // otelhttp middleware creates the labeler 149 labeler, _ := otelhttp.LabelerFromContext(ctx) 150 labeler.Add(webhook.WebhookTypeAttr.With(webhook.WebhookTypeDefaulting)) 151 152 if ac.withContext != nil { 153 ctx = ac.withContext(ctx) 154 } 155 156 logger := logging.FromContext(ctx) 157 switch request.Operation { 158 case admissionv1.Create, admissionv1.Update: 159 default: 160 logger.Info("Unhandled webhook operation, letting it through ", request.Operation) 161 return &admissionv1.AdmissionResponse{Allowed: true} 162 } 163 164 patchBytes, err := ac.mutate(ctx, request) 165 if err != nil { 166 return webhook.MakeErrorStatus("mutation failed: %v", err) 167 } 168 logger.Infof("Kind: %q PatchBytes: %v", request.Kind, string(patchBytes)) 169 170 return &admissionv1.AdmissionResponse{ 171 Patch: patchBytes, 172 Allowed: true, 173 PatchType: func() *admissionv1.PatchType { 174 pt := admissionv1.PatchTypeJSONPatch 175 return &pt 176 }(), 177 } 178 } 179 180 func (ac *reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byte) error { 181 logger := logging.FromContext(ctx) 182 183 rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(ac.handlers)) 184 gvks := make(map[schema.GroupVersionKind]struct{}, len(ac.handlers)+len(ac.callbacks)) 185 for gvk := range ac.handlers { 186 gvks[gvk] = struct{}{} 187 } 188 for gvk := range ac.callbacks { 189 if _, ok := gvks[gvk]; !ok { 190 gvks[gvk] = struct{}{} 191 } 192 } 193 194 for gvk := range gvks { 195 plural := strings.ToLower(flect.Pluralize(gvk.Kind)) 196 197 rules = append(rules, admissionregistrationv1.RuleWithOperations{ 198 Operations: []admissionregistrationv1.OperationType{ 199 admissionregistrationv1.Create, 200 admissionregistrationv1.Update, 201 }, 202 Rule: admissionregistrationv1.Rule{ 203 APIGroups: []string{gvk.Group}, 204 APIVersions: []string{gvk.Version}, 205 Resources: []string{plural, plural + "/status"}, 206 }, 207 }) 208 } 209 210 // Sort the rules by Group, Version, Kind so that things are deterministically ordered. 211 sort.Slice(rules, func(i, j int) bool { 212 lhs, rhs := rules[i], rules[j] 213 if lhs.APIGroups[0] != rhs.APIGroups[0] { 214 return lhs.APIGroups[0] < rhs.APIGroups[0] 215 } 216 if lhs.APIVersions[0] != rhs.APIVersions[0] { 217 return lhs.APIVersions[0] < rhs.APIVersions[0] 218 } 219 return lhs.Resources[0] < rhs.Resources[0] 220 }) 221 222 configuredWebhook, err := ac.mwhlister.Get(ac.key.Name) 223 if err != nil { 224 return fmt.Errorf("error retrieving webhook: %w", err) 225 } 226 227 current := configuredWebhook.DeepCopy() 228 229 if !ac.disableNamespaceOwnership { 230 ns, err := ac.client.CoreV1().Namespaces().Get(ctx, system.Namespace(), metav1.GetOptions{}) 231 if err != nil { 232 return fmt.Errorf("failed to fetch namespace: %w", err) 233 } 234 nsRef := *metav1.NewControllerRef(ns, corev1.SchemeGroupVersion.WithKind("Namespace")) 235 nsRef.Controller = ptr.Bool(false) 236 current.OwnerReferences = []metav1.OwnerReference{nsRef} 237 } 238 239 for i, wh := range current.Webhooks { 240 if wh.Name != current.Name { 241 continue 242 } 243 244 cur := ¤t.Webhooks[i] 245 cur.Rules = rules 246 247 cur.NamespaceSelector = webhook.EnsureLabelSelectorExpressions( 248 cur.NamespaceSelector, 249 &metav1.LabelSelector{ 250 MatchExpressions: []metav1.LabelSelectorRequirement{{ 251 Key: "webhooks.knative.dev/exclude", 252 Operator: metav1.LabelSelectorOpDoesNotExist, 253 }}, 254 }) 255 256 cur.ClientConfig.CABundle = caCert 257 if cur.ClientConfig.Service == nil { 258 return fmt.Errorf("missing service reference for webhook: %s", wh.Name) 259 } 260 cur.ClientConfig.Service.Path = ptr.String(ac.Path()) 261 262 cur.ReinvocationPolicy = ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy) 263 } 264 265 if ok, err := kmp.SafeEqual(configuredWebhook, current); err != nil { 266 return fmt.Errorf("error diffing webhooks: %w", err) 267 } else if !ok { 268 logger.Info("Updating webhook") 269 mwhclient := ac.client.AdmissionregistrationV1().MutatingWebhookConfigurations() 270 if _, err := mwhclient.Update(ctx, current, metav1.UpdateOptions{}); err != nil { 271 return fmt.Errorf("failed to update webhook: %w", err) 272 } 273 } else { 274 logger.Info("Webhook is valid") 275 } 276 return nil 277 } 278 279 func (ac *reconciler) mutate(ctx context.Context, req *admissionv1.AdmissionRequest) ([]byte, error) { 280 kind := req.Kind 281 newBytes := req.Object.Raw 282 oldBytes := req.OldObject.Raw 283 // Why, oh why are these different types... 284 gvk := schema.GroupVersionKind{ 285 Group: kind.Group, 286 Version: kind.Version, 287 Kind: kind.Kind, 288 } 289 290 logger := logging.FromContext(ctx) 291 handler, ok := ac.handlers[gvk] 292 if !ok { 293 if _, ok := ac.callbacks[gvk]; !ok { 294 logger.Error("Unhandled kind: ", gvk) 295 return nil, fmt.Errorf("unhandled kind: %v", gvk) 296 } 297 patches, err := ac.callback(ctx, gvk, req, true /* shouldSetUserInfo */, duck.JSONPatch{}) 298 if err != nil { 299 logger.Errorw("Failed the callback defaulter", zap.Error(err)) 300 // Return the error message as-is to give the defaulter callback 301 // discretion over (our portion of) the message that the user sees. 302 return nil, err 303 } 304 return json.Marshal(patches) 305 } 306 307 // nil values denote absence of `old` (create) or `new` (delete) objects. 308 var oldObj, newObj resourcesemantics.GenericCRD 309 310 if len(newBytes) != 0 { 311 newObj = handler.DeepCopyObject().(resourcesemantics.GenericCRD) 312 err := json.Decode(newBytes, newObj, ac.disallowUnknownFields) 313 if err != nil { 314 return nil, fmt.Errorf("cannot decode incoming new object: %w", err) 315 } 316 } 317 if len(oldBytes) != 0 { 318 oldObj = handler.DeepCopyObject().(resourcesemantics.GenericCRD) 319 err := json.Decode(oldBytes, oldObj, ac.disallowUnknownFields) 320 if err != nil { 321 return nil, fmt.Errorf("cannot decode incoming old object: %w", err) 322 } 323 } 324 var patches duck.JSONPatch 325 326 var err error 327 // Skip this step if the type we're dealing with is a duck type, since it is inherently 328 // incomplete and this will patch away all of the unspecified fields. 329 if _, ok := newObj.(duck.Populatable); !ok { 330 // Add these before defaulting fields, otherwise defaulting may cause an illegal patch 331 // because it expects the round tripped through Golang fields to be present already. 332 rtp, err := roundTripPatch(newBytes, newObj) 333 if err != nil { 334 return nil, fmt.Errorf("cannot create patch for round tripped newBytes: %w", err) 335 } 336 patches = append(patches, rtp...) 337 } 338 339 // Set up the context for defaulting and validation 340 if oldObj != nil { 341 // Copy the old object and set defaults so that we don't reject our own 342 // defaulting done earlier in the webhook. 343 oldObj = oldObj.DeepCopyObject().(resourcesemantics.GenericCRD) 344 oldObj.SetDefaults(ctx) 345 346 s, ok := oldObj.(apis.HasSpec) 347 if ok { 348 setUserInfoAnnotations(ctx, s, req.Resource.Group) 349 } 350 351 if req.SubResource == "" { 352 ctx = apis.WithinUpdate(ctx, oldObj) 353 } else { 354 ctx = apis.WithinSubResourceUpdate(ctx, oldObj, req.SubResource) 355 } 356 } else { 357 ctx = apis.WithinCreate(ctx) 358 } 359 ctx = apis.WithUserInfo(ctx, &req.UserInfo) 360 361 // Default the new object. 362 if patches, err = setDefaults(ctx, patches, newObj); err != nil { 363 logger.Errorw("Failed the resource specific defaulter", zap.Error(err)) 364 // Return the error message as-is to give the defaulter callback 365 // discretion over (our portion of) the message that the user sees. 366 return nil, err 367 } 368 369 if patches, err = ac.setUserInfoAnnotations(ctx, patches, newObj, req.Resource.Group); err != nil { 370 logger.Errorw("Failed the resource user info annotator", zap.Error(err)) 371 return nil, err 372 } 373 374 if patches, err = ac.callback(ctx, gvk, req, false /* shouldSetUserInfo */, patches); err != nil { 375 logger.Errorw("Failed the callback defaulter", zap.Error(err)) 376 // Return the error message as-is to give the defaulter callback 377 // discretion over (our portion of) the message that the user sees. 378 return nil, err 379 } 380 381 // None of the validators will accept a nil value for newObj. 382 if newObj == nil { 383 return nil, errMissingNewObject 384 } 385 return json.Marshal(patches) 386 } 387 388 func (ac *reconciler) setUserInfoAnnotations(ctx context.Context, patches duck.JSONPatch, new resourcesemantics.GenericCRD, groupName string) (duck.JSONPatch, error) { 389 if new == nil { 390 return patches, nil 391 } 392 nh, ok := new.(apis.HasSpec) 393 if !ok { 394 return patches, nil 395 } 396 397 b, a := new.DeepCopyObject().(apis.HasSpec), nh 398 399 setUserInfoAnnotations(ctx, nh, groupName) 400 401 patch, err := duck.CreatePatch(b, a) 402 if err != nil { 403 return nil, err 404 } 405 return append(patches, patch...), nil 406 } 407 408 func (ac *reconciler) callback(ctx context.Context, gvk schema.GroupVersionKind, req *admissionv1.AdmissionRequest, shouldSetUserInfo bool, patches duck.JSONPatch) (duck.JSONPatch, error) { 409 // Get callback. 410 callback, ok := ac.callbacks[gvk] 411 if !ok { 412 return patches, nil 413 } 414 415 // Check if request operation is a supported webhook operation. 416 if _, isSupported := callback.supportedVerbs[req.Operation]; !isSupported { 417 return patches, nil 418 } 419 420 oldBytes := req.OldObject.Raw 421 newBytes := req.Object.Raw 422 423 before := &unstructured.Unstructured{} 424 after := &unstructured.Unstructured{} 425 426 // Get unstructured object. 427 if err := json.Unmarshal(newBytes, before); err != nil { 428 return nil, fmt.Errorf("cannot decode object: %w", err) 429 } 430 // Copy before in after unstructured objects. 431 before.DeepCopyInto(after) 432 433 // Setup context. 434 if len(oldBytes) != 0 { 435 if req.SubResource == "" { 436 ctx = apis.WithinUpdate(ctx, before) 437 } else { 438 ctx = apis.WithinSubResourceUpdate(ctx, before, req.SubResource) 439 } 440 } else { 441 ctx = apis.WithinCreate(ctx) 442 } 443 ctx = apis.WithUserInfo(ctx, &req.UserInfo) 444 445 // Call callback passing after. 446 if err := callback.function(ctx, after); err != nil { 447 return patches, err 448 } 449 450 if shouldSetUserInfo { 451 setUserInfoAnnotations(adaptUnstructuredHasSpecCtx(ctx, req), unstructuredHasSpec{after}, req.Resource.Group) 452 } 453 454 // Create patches. 455 patch, err := duck.CreatePatch(before.Object, after.Object) 456 return append(patches, patch...), err 457 } 458 459 // roundTripPatch generates the JSONPatch that corresponds to round tripping the given bytes through 460 // the Golang type (JSON -> Golang type -> JSON). Because it is not always true that 461 // bytes == json.Marshal(json.Unmarshal(bytes)). 462 // 463 // For example, if bytes did not contain a 'spec' field and the Golang type specifies its 'spec' 464 // field without omitempty, then by round tripping through the Golang type, we would have added 465 // `'spec': {}`. 466 func roundTripPatch(bytes []byte, unmarshalled interface{}) (duck.JSONPatch, error) { 467 if unmarshalled == nil { 468 return duck.JSONPatch{}, nil 469 } 470 marshaledBytes, err := json.Marshal(unmarshalled) 471 if err != nil { 472 return nil, fmt.Errorf("cannot marshal interface: %w", err) 473 } 474 return jsonpatch.CreatePatch(bytes, marshaledBytes) 475 } 476 477 // setDefaults simply leverages apis.Defaultable to set defaults. 478 func setDefaults(ctx context.Context, patches duck.JSONPatch, crd resourcesemantics.GenericCRD) (duck.JSONPatch, error) { 479 before, after := crd.DeepCopyObject(), crd 480 after.SetDefaults(ctx) 481 482 patch, err := duck.CreatePatch(before, after) 483 if err != nil { 484 return nil, err 485 } 486 487 return append(patches, patch...), nil 488 } 489 490 func ptrReinvocationPolicyType(r admissionregistrationv1.ReinvocationPolicyType) *admissionregistrationv1.ReinvocationPolicyType { 491 return &r 492 }