knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/psbinding/reconciler.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 "reflect" 24 25 "go.uber.org/zap" 26 "golang.org/x/sync/errgroup" 27 corev1 "k8s.io/api/core/v1" 28 "k8s.io/apimachinery/pkg/api/equality" 29 apierrs "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/types" 33 "k8s.io/apimachinery/pkg/util/sets" 34 "k8s.io/client-go/dynamic" 35 corev1listers "k8s.io/client-go/listers/core/v1" 36 "k8s.io/client-go/tools/cache" 37 "k8s.io/client-go/tools/record" 38 "knative.dev/pkg/apis" 39 "knative.dev/pkg/apis/duck" 40 duckv1 "knative.dev/pkg/apis/duck/v1" 41 "knative.dev/pkg/controller" 42 "knative.dev/pkg/kmeta" 43 "knative.dev/pkg/logging" 44 pkgreconciler "knative.dev/pkg/reconciler" 45 "knative.dev/pkg/tracker" 46 ) 47 48 // SubResourcesReconcilerInterface is used to reconcile binding related 49 // sub-resources. Reconcile is executed after Binding's ReconcileSubject 50 // and ReconcileDeletion will be executed before Binding's ReconcileDeletion 51 type SubResourcesReconcilerInterface interface { 52 Reconcile(ctx context.Context, fb Bindable) error 53 ReconcileDeletion(ctx context.Context, fb Bindable) error 54 } 55 56 var jsonLabelPatch = map[string]interface{}{ 57 "metadata": map[string]interface{}{ 58 "labels": map[string]string{duck.BindingIncludeLabel: "true"}, 59 }, 60 } 61 62 // BaseReconciler helps implement controller.Reconciler for Binding resources. 63 type BaseReconciler struct { 64 pkgreconciler.LeaderAwareFuncs 65 66 // The GVR of the "primary key" resource for this reconciler. 67 // This is used along with the DynamicClient for updating the status 68 // and managing finalizers of the resources being reconciled. 69 GVR schema.GroupVersionResource 70 71 // Get is a callback that fetches the Bindable with the provided name 72 // and namespace (for this GVR). 73 Get func(namespace string, name string) (Bindable, error) 74 75 // WithContext is a callback that infuses the context supplied to 76 // Do/Undo with additional context to enable them to complete their 77 // respective tasks. 78 WithContext BindableContext 79 80 // DynamicClient is used to patch subjects and apply mutations to 81 // Bindable resources (determined by GVR) to reflect status updates. 82 DynamicClient dynamic.Interface 83 84 // Factory is used for producing listers for the object references we 85 // encounter. 86 Factory duck.InformerFactory 87 88 // The tracker builds an index of what resources are watching other 89 // resources so that we can immediately react to changes to changes in 90 // tracked resources. 91 Tracker tracker.Interface 92 93 // Recorder is an event recorder for recording Event resources to the 94 // Kubernetes API. 95 Recorder record.EventRecorder 96 97 // Namespace Lister 98 NamespaceLister corev1listers.NamespaceLister 99 100 // Sub-resources reconciler. Used to reconcile Binding related resources 101 SubResourcesReconciler SubResourcesReconcilerInterface 102 } 103 104 // Check that our Reconciler implements controller.Reconciler 105 var ( 106 _ controller.Reconciler = (*BaseReconciler)(nil) 107 _ pkgreconciler.LeaderAware = (*BaseReconciler)(nil) 108 ) 109 110 // Reconcile implements controller.Reconciler 111 func (r *BaseReconciler) Reconcile(ctx context.Context, key string) error { 112 // Convert the namespace/name string into a distinct namespace and name 113 namespace, name, err := cache.SplitMetaNamespaceKey(key) 114 if err != nil { 115 logging.FromContext(ctx).Error("invalid resource key: ", key) 116 return nil //nolint:nilerr 117 } 118 119 // Only the leader should reconcile binding resources. 120 if !r.IsLeaderFor(types.NamespacedName{ 121 Namespace: namespace, 122 Name: name, 123 }) { 124 return controller.NewSkipKey(key) 125 } 126 127 // Get the resource with this namespace/name. 128 original, err := r.Get(namespace, name) 129 if apierrs.IsNotFound(err) { 130 // The resource may no longer exist, in which case we stop processing. 131 logging.FromContext(ctx).Errorf("resource %q no longer exists", key) 132 return nil 133 } else if err != nil { 134 return err 135 } 136 // Don't modify the informers copy. 137 resource := original.DeepCopyObject().(Bindable) 138 139 // Reconcile this copy of the resource and then write back any status 140 // updates regardless of whether the reconciliation errored out. 141 reconcileErr := r.reconcile(ctx, resource) 142 if equality.Semantic.DeepEqual(original.GetBindingStatus(), resource.GetBindingStatus()) { 143 // If we didn't change anything then don't call updateStatus. 144 // This is important because the copy we loaded from the informer's 145 // cache may be stale and we don't want to overwrite a prior update 146 // to status with this stale state. 147 } else if err = r.UpdateStatus(ctx, resource); err != nil { 148 logging.FromContext(ctx).Warnw("Failed to update resource status", zap.Error(err)) 149 r.Recorder.Eventf(resource, corev1.EventTypeWarning, "UpdateFailed", 150 "Failed to update status for %q: %v", resource.GetName(), err) 151 return err 152 } 153 if reconcileErr != nil { 154 r.Recorder.Event(resource, corev1.EventTypeWarning, "InternalError", reconcileErr.Error()) 155 } 156 return reconcileErr 157 } 158 159 // reconcile is a reference implementation of a simple Binding flow, it and 160 // Reconcile may be overridden and the exported helpers used to implement a 161 // customized reconciliation flow. 162 func (r *BaseReconciler) reconcile(ctx context.Context, fb Bindable) error { 163 if fb.GetDeletionTimestamp() != nil { 164 // Check for a DeletionTimestamp. If present, elide the normal 165 // reconcile logic and do our finalizer handling. 166 if r.SubResourcesReconciler != nil { 167 // If a SubResourceReconciler is defined, finalize related resources 168 if err := r.SubResourcesReconciler.ReconcileDeletion(ctx, fb); err != nil { 169 return err 170 } 171 } 172 return r.ReconcileDeletion(ctx, fb) 173 } 174 // Make sure that our conditions have been initialized. 175 fb.GetBindingStatus().InitializeConditions() 176 177 // Make sure that the resource has a Finalizer configured, which 178 // enables us to undo our binding upon deletion. 179 if err := r.EnsureFinalizer(ctx, fb); err != nil { 180 return err 181 } 182 183 // Perform our Binding's Do() method on the subject(s) of the Binding. 184 if err := r.ReconcileSubject(ctx, fb, fb.Do); err != nil { 185 return err 186 } 187 if r.SubResourcesReconciler != nil { 188 // If a SubResourceReconciler is defined, reconcile related resources 189 if err := r.SubResourcesReconciler.Reconcile(ctx, fb); err != nil { 190 return err 191 } 192 } 193 194 // Update the observed generation once we have successfully reconciled 195 // our spec. 196 fb.GetBindingStatus().SetObservedGeneration(fb.GetGeneration()) 197 return nil 198 } 199 200 // ReconcileDeletion handles reconcile a resource that is being deleted, which 201 // amounts to properly finalizing the resource. 202 func (r *BaseReconciler) ReconcileDeletion(ctx context.Context, fb Bindable) error { 203 // If we are not the controller finalizing this resource, then we 204 // are done. 205 if !r.IsFinalizing(ctx, fb) { 206 return nil 207 } 208 209 // If it is our turn to finalize the Binding, then first undo the effect 210 // of our Binding on the resource. 211 logging.FromContext(ctx).Info("Removing the binding for ", fb.GetName()) 212 if err := r.ReconcileSubject(ctx, fb, fb.Undo); apierrs.IsNotFound(err) || apierrs.IsForbidden(err) { 213 // If the subject has been deleted, then there is nothing to undo. 214 } else if err != nil { 215 return err 216 } 217 218 // Once the Binding has been undone, remove our finalizer allowing the 219 // Binding resource's deletion to progress. 220 return r.RemoveFinalizer(ctx, fb) 221 } 222 223 // IsFinalizing determines whether it is our reconciler's turn to finalize a 224 // resource in the process of being deleted. This means that our finalizer is 225 // at the head of the metadata.finalizers list. 226 func (r *BaseReconciler) IsFinalizing(ctx context.Context, fb kmeta.Accessor) bool { 227 return len(fb.GetFinalizers()) != 0 && fb.GetFinalizers()[0] == r.GVR.GroupResource().String() 228 } 229 230 // EnsureFinalizer makes sure that the provided resource has a finalizer in the 231 // form of this BaseReconciler's GVR's stringified GroupResource. 232 func (r *BaseReconciler) EnsureFinalizer(ctx context.Context, fb kmeta.Accessor) error { 233 // If it has the finalizer, then we're done. 234 finalizers := sets.New[string](fb.GetFinalizers()...) 235 if finalizers.Has(r.GVR.GroupResource().String()) { 236 return nil 237 } 238 239 // If it doesn't have our finalizer, then synthesize a patch to add it. 240 patch, err := json.Marshal(map[string]interface{}{ 241 "metadata": map[string]interface{}{ 242 "finalizers": append(fb.GetFinalizers(), r.GVR.GroupResource().String()), 243 "resourceVersion": fb.GetResourceVersion(), 244 }, 245 }) 246 if err != nil { 247 return err 248 } 249 250 // ... and apply it. 251 _, err = r.DynamicClient.Resource(r.GVR).Namespace(fb.GetNamespace()).Patch(ctx, fb.GetName(), 252 types.MergePatchType, patch, metav1.PatchOptions{}) 253 return err 254 } 255 256 // RemoveFinalizer is the dual of EnsureFinalizer, it removes our finalizer from 257 // the Binding resource 258 func (r *BaseReconciler) RemoveFinalizer(ctx context.Context, fb kmeta.Accessor) error { 259 logging.FromContext(ctx).Info("Removing Finalizer") 260 261 // Synthesize a patch removing our finalizer from the head of the 262 // finalizer list. 263 patch, err := json.Marshal(map[string]interface{}{ 264 "metadata": map[string]interface{}{ 265 "finalizers": fb.GetFinalizers()[1:], 266 "resourceVersion": fb.GetResourceVersion(), 267 }, 268 }) 269 if err != nil { 270 return err 271 } 272 273 // ... and apply it. 274 _, err = r.DynamicClient.Resource(r.GVR).Namespace(fb.GetNamespace()).Patch(ctx, fb.GetName(), 275 types.MergePatchType, patch, metav1.PatchOptions{}) 276 return err 277 } 278 279 func (r *BaseReconciler) labelNamespace(ctx context.Context, subject tracker.Reference) error { 280 namespaceObject, err := r.NamespaceLister.Get(subject.Namespace) 281 if apierrs.IsNotFound(err) { 282 logging.FromContext(ctx).Info("Error getting namespace (not found): ", err) 283 return err 284 } else if err != nil { 285 logging.FromContext(ctx).Info("Error getting namespace: ", err) 286 return err 287 } 288 289 labels := namespaceObject.GetLabels() 290 if labels[duck.BindingIncludeLabel] != "" || labels[duck.BindingExcludeLabel] != "" { 291 return nil 292 } 293 294 patch, err := json.Marshal(jsonLabelPatch) 295 if err != nil { 296 logging.FromContext(ctx).Infof("Error generating json patch: %v, to namespace: %s", err, subject.Namespace) 297 return nil 298 } 299 300 // Determine the GroupVersionResource of the subject reference 301 gvr := schema.GroupVersionResource{ 302 Group: "", 303 Version: "v1", 304 Resource: "namespaces", 305 } 306 307 _, err = r.DynamicClient.Resource(gvr).Patch(ctx, subject.Namespace, types.MergePatchType, patch, metav1.PatchOptions{}) 308 if err != nil { 309 logging.FromContext(ctx).Infof("Error applying patch to namespace: %s: %v", subject.Namespace, err) 310 return err 311 } 312 313 return nil 314 } 315 316 // ReconcileSubject handles applying the provided Binding "mutation" (Do or 317 // Undo) to the Binding's subject(s). 318 func (r *BaseReconciler) ReconcileSubject(ctx context.Context, fb Bindable, mutation Mutation) error { 319 // Access the subject of our Binding and have the tracker queue this 320 // Bindable whenever it changes. 321 subject := fb.GetSubject() 322 if err := r.Tracker.TrackReference(subject, fb); err != nil { 323 logging.FromContext(ctx).Errorf("Error tracking subject %v: %v", subject, err) 324 return err 325 } 326 327 // Determine the GroupVersionResource of the subject reference 328 gv, err := schema.ParseGroupVersion(subject.APIVersion) 329 if err != nil { 330 logging.FromContext(ctx).Errorf("Error parsing GroupVersion %v: %v", subject.APIVersion, err) 331 return err 332 } 333 gvr := apis.KindToResource(gv.WithKind(subject.Kind)) 334 335 // Use the GVR of the subject(s) to get ahold of a lister that we can 336 // use to fetch our PodSpecable resources. 337 _, lister, err := r.Factory.Get(ctx, gvr) 338 if err != nil { 339 logging.FromContext(ctx).Errorf("Error getting a lister for resource '%+v': %v", gvr, err) 340 fb.GetBindingStatus().MarkBindingUnavailable("SubjectUnavailable", err.Error()) 341 return err 342 } 343 344 // Based on the type of subject reference, build up a list of referents. 345 var referents []*duckv1.WithPod 346 if subject.Name != "" { 347 // If name is specified, then fetch it from the lister and turn 348 // it into a singleton list. 349 psObj, err := lister.ByNamespace(subject.Namespace).Get(subject.Name) 350 if apierrs.IsNotFound(err) { 351 fb.GetBindingStatus().MarkBindingUnavailable("SubjectMissing", err.Error()) 352 return err 353 } else if err != nil { 354 return fmt.Errorf("error fetching Pod Speccable %v: %w", subject, err) 355 } 356 err = r.labelNamespace(ctx, subject) 357 if err != nil { 358 return err 359 } 360 referents = append(referents, psObj.(*duckv1.WithPod)) 361 } else { 362 // Otherwise, the subject is referenced by selector, so compile 363 // the selector and pass it to the lister. 364 selector, err := metav1.LabelSelectorAsSelector(subject.Selector) 365 if err != nil { 366 return err 367 } 368 psObjs, err := lister.ByNamespace(subject.Namespace).List(selector) 369 if err != nil { 370 return fmt.Errorf("error fetching Pod Speccable %v: %w", subject, err) 371 } 372 err = r.labelNamespace(ctx, subject) 373 if err != nil { 374 return err 375 } 376 // Type cast the returned resources into our referent list. 377 for _, psObj := range psObjs { 378 referents = append(referents, psObj.(*duckv1.WithPod)) 379 } 380 } 381 382 // Callback into the user's code to setup the context with additional 383 // information needed to perform the mutation. 384 if r.WithContext != nil { 385 ctx, err = r.WithContext(ctx, fb) 386 if err != nil { 387 return err 388 } 389 } 390 391 // For each of the referents, apply the mutation. 392 eg := errgroup.Group{} 393 for _, ps := range referents { 394 eg.Go(func() error { 395 // Do the binding to the pod specable. 396 orig := ps.DeepCopy() 397 mutation(ctx, ps) 398 399 // If nothing changed, then bail early. 400 if equality.Semantic.DeepEqual(orig, ps) { 401 return nil 402 } 403 404 // If we encountered changes, then synthesize and apply 405 // a patch. 406 patchBytes, err := duck.CreateBytePatch(orig, ps) 407 if err != nil { 408 return err 409 } 410 411 // TODO(mattmoor): This might fail because a binding changed after 412 // a Job started or completed, which can be fine. Consider treating 413 // certain error codes as acceptable. 414 _, err = r.DynamicClient.Resource(gvr).Namespace(ps.Namespace).Patch( 415 ctx, ps.Name, types.JSONPatchType, patchBytes, metav1.PatchOptions{}) 416 if err != nil { 417 return fmt.Errorf("failed binding subject %s: %w", ps.Name, err) 418 } 419 return nil 420 }) 421 } 422 423 // Based on the success of the referent binding, update the Binding's readiness. 424 if err := eg.Wait(); err != nil { 425 fb.GetBindingStatus().MarkBindingUnavailable("BindingFailed", err.Error()) 426 return err 427 } 428 fb.GetBindingStatus().MarkBindingAvailable() 429 return nil 430 } 431 432 // UpdateStatus updates the status of the resource. Caller is responsible for 433 // checking for semantic differences before calling. 434 func (r *BaseReconciler) UpdateStatus(ctx context.Context, desired Bindable) error { 435 actual, err := r.Get(desired.GetNamespace(), desired.GetName()) 436 if err != nil { 437 logging.FromContext(ctx).Errorw("Error fetching actual", zap.Error(err)) 438 return err 439 } 440 441 // Convert to unstructured for use with the dynamic client. 442 ua, err := duck.ToUnstructured(actual) 443 if err != nil { 444 logging.FromContext(ctx).Errorw("Error converting actual", zap.Error(err)) 445 return err 446 } 447 ud, err := duck.ToUnstructured(desired) 448 if err != nil { 449 logging.FromContext(ctx).Errorw("Error converting desired", zap.Error(err)) 450 return err 451 } 452 453 // One last check that status changed. 454 actualStatus := ua.Object["status"] 455 desiredStatus := ud.Object["status"] 456 if reflect.DeepEqual(actualStatus, desiredStatus) { 457 return nil 458 } 459 460 // Copy the status over to the refetched resource to avoid updating 461 // anything other than status. 462 forUpdate := ua 463 forUpdate.Object["status"] = desiredStatus 464 _, err = r.DynamicClient.Resource(r.GVR).Namespace(desired.GetNamespace()).UpdateStatus( 465 ctx, forUpdate, metav1.UpdateOptions{}) 466 return err 467 }