knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/psbinding/psbinding.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 psbinding 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "sort" 24 "strings" 25 26 "github.com/gobuffalo/flect" 27 "go.uber.org/zap" 28 admissionv1 "k8s.io/api/admission/v1" 29 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 30 "k8s.io/apimachinery/pkg/api/equality" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/labels" 33 "k8s.io/apimachinery/pkg/runtime/schema" 34 "k8s.io/apimachinery/pkg/util/sets" 35 "k8s.io/client-go/kubernetes" 36 admissionlisters "k8s.io/client-go/listers/admissionregistration/v1" 37 corelisters "k8s.io/client-go/listers/core/v1" 38 "knative.dev/pkg/apis/duck" 39 duckv1 "knative.dev/pkg/apis/duck/v1" 40 "knative.dev/pkg/controller" 41 "knative.dev/pkg/logging" 42 "knative.dev/pkg/ptr" 43 pkgreconciler "knative.dev/pkg/reconciler" 44 "knative.dev/pkg/system" 45 "knative.dev/pkg/webhook" 46 certresources "knative.dev/pkg/webhook/certificates/resources" 47 ) 48 49 // ReconcilerOption is a function to modify the Reconciler. 50 type ReconcilerOption func(*Reconciler) 51 52 // WithSelector specifies the selector for the webhook. 53 func WithSelector(s metav1.LabelSelector) ReconcilerOption { 54 return func(r *Reconciler) { 55 r.selector = s 56 } 57 } 58 59 func NewReconciler( 60 name, path, secretName string, 61 client kubernetes.Interface, 62 mwhLister admissionlisters.MutatingWebhookConfigurationLister, 63 secretLister corelisters.SecretLister, 64 withContext BindableContext, 65 options ...ReconcilerOption, 66 ) *Reconciler { 67 r := &Reconciler{ 68 Name: name, 69 HandlerPath: path, 70 SecretName: secretName, 71 72 // This is the user-provided context-decorator, which allows 73 // them to infuse the context passed to Do/Undo. 74 WithContext: withContext, 75 76 Client: client, 77 MWHLister: mwhLister, 78 SecretLister: secretLister, 79 selector: ExclusionSelector, // Use ExclusionSelector by default. 80 } 81 82 // Apply options. 83 for _, opt := range options { 84 opt(r) 85 } 86 87 return r 88 } 89 90 // Reconciler implements an AdmissionController for altering PodSpecable 91 // resources that are the subject of a particular type of Binding. 92 // The two key methods are: 93 // 1. reconcileMutatingWebhook: which enumerates all of the Bindings and 94 // compiles a list of resource types that should be intercepted by our 95 // webhook. It also builds an index that can be used to efficiently 96 // handle Admit requests. 97 // 2. Admit: which leverages the index built by the Reconciler to apply 98 // mutations to resources. 99 type Reconciler struct { 100 pkgreconciler.LeaderAwareFuncs 101 102 Name string 103 HandlerPath string 104 SecretName string 105 106 Client kubernetes.Interface 107 MWHLister admissionlisters.MutatingWebhookConfigurationLister 108 SecretLister corelisters.SecretLister 109 ListAll ListAll 110 111 // WithContext is a callback that infuses the context supplied to 112 // Do/Undo with additional context to enable them to complete their 113 // respective tasks. 114 WithContext BindableContext 115 116 selector metav1.LabelSelector 117 118 index index 119 } 120 121 var ( 122 _ controller.Reconciler = (*Reconciler)(nil) 123 _ pkgreconciler.LeaderAware = (*Reconciler)(nil) 124 _ webhook.AdmissionController = (*Reconciler)(nil) 125 ) 126 127 // We need to specifically exclude our deployment(s) from consideration, but this provides a way 128 // of excluding other things as well. 129 var ( 130 ExclusionSelector = metav1.LabelSelector{ 131 MatchExpressions: []metav1.LabelSelectorRequirement{{ 132 Key: duck.BindingExcludeLabel, 133 Operator: metav1.LabelSelectorOpNotIn, 134 Values: []string{"true"}, 135 }}, 136 // TODO(mattmoor): Consider also having a GVR-based one, e.g. 137 // foobindings.blah.knative.dev/exclude: "true" 138 } 139 InclusionSelector = metav1.LabelSelector{ 140 MatchExpressions: []metav1.LabelSelectorRequirement{{ 141 Key: duck.BindingIncludeLabel, 142 Operator: metav1.LabelSelectorOpIn, 143 Values: []string{"true"}, 144 }}, 145 // TODO(mattmoor): Consider also having a GVR-based one, e.g. 146 // foobindings.blah.knative.dev/include: "true" 147 } 148 ) 149 150 // Reconcile implements controller.Reconciler 151 func (ac *Reconciler) Reconcile(ctx context.Context, key string) error { 152 // Look up the webhook secret, and fetch the CA cert bundle. 153 secret, err := ac.SecretLister.Secrets(system.Namespace()).Get(ac.SecretName) 154 if err != nil { 155 logging.FromContext(ctx).Errorw("Error fetching secret", zap.Error(err)) 156 return err 157 } 158 caCert, ok := secret.Data[certresources.CACert] 159 if !ok { 160 return fmt.Errorf("secret %q is missing %q key", ac.SecretName, certresources.CACert) 161 } 162 163 // Reconcile the webhook configuration. 164 return ac.reconcileMutatingWebhook(ctx, caCert) 165 } 166 167 // Path implements AdmissionController 168 func (ac *Reconciler) Path() string { 169 return ac.HandlerPath 170 } 171 172 // Admit implements AdmissionController 173 func (ac *Reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 174 switch request.Operation { 175 case admissionv1.Create, admissionv1.Update: 176 default: 177 logging.FromContext(ctx).Info("Unhandled webhook operation, letting it through ", request.Operation) 178 return &admissionv1.AdmissionResponse{Allowed: true} 179 } 180 181 orig := &duckv1.WithPod{} 182 if err := json.Unmarshal(request.Object.Raw, orig); err != nil { 183 return webhook.MakeErrorStatus("unable to decode object: %v", err) 184 } 185 186 // Look up the Bindables for this resource. 187 fbs := ac.index.lookUp(exactKey{ 188 Group: request.Kind.Group, 189 Kind: request.Kind.Kind, 190 Namespace: request.Namespace, 191 Name: orig.Name, 192 }, 193 labels.Set(orig.Labels)) 194 if len(fbs) == 0 { 195 // This doesn't apply! 196 return &admissionv1.AdmissionResponse{Allowed: true} 197 } 198 199 // Copy the subject state. 200 mutated := orig.DeepCopy() 201 202 // Apply the Bindables to the copy of the subject state. If conflicts occur, for example because multiple Bindables 203 // make incompatible changes, the reconciler will attempt to correct the state later. 204 for _, fb := range fbs { 205 var bindingContext context.Context 206 // Callback into the user's code to setup the context with additional 207 // information needed to perform the mutation. 208 if ac.WithContext != nil { 209 var err error 210 bindingContext, err = ac.WithContext(ctx, fb) 211 if err != nil { 212 return webhook.MakeErrorStatus("unable to setup binding context: %v", err) 213 } 214 } else { 215 bindingContext = ctx 216 } 217 218 // Mutate the copy of the subject state according to the deletion state of the Bindable. 219 if fb.GetDeletionTimestamp() != nil { 220 fb.Undo(bindingContext, mutated) 221 } else { 222 fb.Do(bindingContext, mutated) 223 } 224 } 225 226 // Synthesize a patch from the changes and return it in our AdmissionResponse 227 patchBytes, err := duck.CreateBytePatch(orig, mutated) 228 if err != nil { 229 return webhook.MakeErrorStatus("unable to create patch with binding: %v", err) 230 } 231 return &admissionv1.AdmissionResponse{ 232 Patch: patchBytes, 233 Allowed: true, 234 PatchType: func() *admissionv1.PatchType { 235 pt := admissionv1.PatchTypeJSONPatch 236 return &pt 237 }(), 238 } 239 } 240 241 func (ac *Reconciler) reconcileMutatingWebhook(ctx context.Context, caCert []byte) error { 242 // Build a deduplicated list of all of the GVKs we see. 243 gks := map[schema.GroupKind]sets.Set[string]{} 244 245 // When reconciling the webhook, enumerate all of the bindings, so that 246 // we can index them to efficiently respond to webhook requests. 247 fbs, err := ac.ListAll() 248 if err != nil { 249 return err 250 } 251 252 ib := newIndexBuilder() 253 for _, fb := range fbs { 254 ref := fb.GetSubject() 255 gv, err := schema.ParseGroupVersion(ref.APIVersion) 256 if err != nil { 257 return err 258 } 259 gk := schema.GroupKind{ 260 Group: gv.Group, 261 Kind: ref.Kind, 262 } 263 set := gks[gk] 264 if set == nil { 265 set = make(sets.Set[string], 1) 266 } 267 set.Insert(gv.Version) 268 gks[gk] = set 269 270 if ref.Name != "" { 271 ib.associate(exactKey{ 272 Group: gk.Group, 273 Kind: gk.Kind, 274 Namespace: ref.Namespace, 275 Name: ref.Name, 276 }, 277 fb) 278 } else { 279 selector, err := metav1.LabelSelectorAsSelector(ref.Selector) 280 if err != nil { 281 return err 282 } 283 ib.associateSelection(inexactKey{ 284 Group: gk.Group, 285 Kind: gk.Kind, 286 Namespace: ref.Namespace, 287 }, 288 selector, fb) 289 } 290 } 291 292 // Update the index. 293 ib.build(&ac.index) 294 295 // After we've updated our indices, bail out unless we are the leader. 296 // Only the leader should be mutating the webhook. 297 if !ac.IsLeaderFor(sentinel) { 298 // We don't use controller.NewSkipKey here because we did do 299 // some amount of processing and the timing information may be 300 // useful. 301 return nil 302 } 303 304 rules := make([]admissionregistrationv1.RuleWithOperations, 0, len(gks)) 305 for gk, versions := range gks { 306 plural := strings.ToLower(flect.Pluralize(gk.Kind)) 307 308 rules = append(rules, admissionregistrationv1.RuleWithOperations{ 309 Operations: []admissionregistrationv1.OperationType{ 310 admissionregistrationv1.Create, 311 admissionregistrationv1.Update, 312 }, 313 Rule: admissionregistrationv1.Rule{ 314 APIGroups: []string{gk.Group}, 315 APIVersions: sets.List(versions), 316 Resources: []string{plural + "/*"}, 317 }, 318 }) 319 } 320 321 // Sort the rules by Group, Version, Kind so that things are deterministically ordered. 322 sort.Slice(rules, func(i, j int) bool { 323 lhs, rhs := rules[i], rules[j] 324 if lhs.APIGroups[0] != rhs.APIGroups[0] { 325 return lhs.APIGroups[0] < rhs.APIGroups[0] 326 } 327 if lhs.APIVersions[0] != rhs.APIVersions[0] { 328 return lhs.APIVersions[0] < rhs.APIVersions[0] 329 } 330 return lhs.Resources[0] < rhs.Resources[0] 331 }) 332 333 configuredWebhook, err := ac.MWHLister.Get(ac.Name) 334 if err != nil { 335 return fmt.Errorf("error retrieving webhook: %w", err) 336 } 337 current := configuredWebhook.DeepCopy() 338 339 // Use the "Equivalent" match policy so that we don't need to enumerate versions for same-types. 340 // This is only supported by 1.15+ clusters. 341 matchPolicy := admissionregistrationv1.Equivalent 342 343 for i, wh := range current.Webhooks { 344 if wh.Name != current.Name { 345 continue 346 } 347 cur := ¤t.Webhooks[i] 348 selector := webhook.EnsureLabelSelectorExpressions(cur.NamespaceSelector, &ac.selector) 349 350 cur.MatchPolicy = &matchPolicy 351 cur.Rules = rules 352 cur.NamespaceSelector = selector 353 cur.ObjectSelector = selector // 1.15+ only 354 cur.ClientConfig.CABundle = caCert 355 if cur.ClientConfig.Service == nil { 356 return fmt.Errorf("missing service reference for webhook: %s", wh.Name) 357 } 358 cur.ClientConfig.Service.Path = ptr.String(ac.Path()) 359 cur.ReinvocationPolicy = ptrReinvocationPolicyType(admissionregistrationv1.IfNeededReinvocationPolicy) 360 } 361 362 if ok := equality.Semantic.DeepEqual(configuredWebhook, current); !ok { 363 logging.FromContext(ctx).Info("Updating webhook") 364 mwhclient := ac.Client.AdmissionregistrationV1().MutatingWebhookConfigurations() 365 if _, err := mwhclient.Update(ctx, current, metav1.UpdateOptions{}); err != nil { 366 return fmt.Errorf("failed to update webhook: %w", err) 367 } 368 } else { 369 logging.FromContext(ctx).Info("Webhook is valid") 370 } 371 return nil 372 } 373 374 func ptrReinvocationPolicyType(r admissionregistrationv1.ReinvocationPolicyType) *admissionregistrationv1.ReinvocationPolicyType { 375 return &r 376 }