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  }