diff --git a/reconcilers/aggregate.go b/reconcilers/aggregate.go index dbcc148..2e0645c 100644 --- a/reconcilers/aggregate.go +++ b/reconcilers/aggregate.go @@ -104,6 +104,22 @@ type AggregateReconciler[Type client.Object] struct { // +optional Sanitize func(resource Type) interface{} + // BeforeReconcile is called first thing for each reconcile request. A modified context may be + // returned. Errors are returned immediately. + // + // If BeforeReconcile is not defined, there is no effect. + // + // +optional + BeforeReconcile func(ctx context.Context, req Request) (context.Context, Result, error) + + // AfterReconcile is called following all work for the reconcile request. The result and error + // are provided and may be modified before returning. + // + // If AfterReconcile is not defined, the result and error are returned directly. + // + // +optional + AfterReconcile func(ctx context.Context, req Request, res Result, err error) (Result, error) + Config Config // stamp manages the lifecycle of the aggregated resource. @@ -128,6 +144,16 @@ func (r *AggregateReconciler[T]) init() { return resource, nil } } + if r.BeforeReconcile == nil { + r.BeforeReconcile = func(ctx context.Context, req reconcile.Request) (context.Context, reconcile.Result, error) { + return ctx, Result{}, nil + } + } + if r.AfterReconcile == nil { + r.AfterReconcile = func(ctx context.Context, req reconcile.Request, res reconcile.Result, err error) (reconcile.Result, error) { + return res, err + } + } r.stamp = &ResourceManager[T]{ Name: r.Name, @@ -231,6 +257,14 @@ func (r *AggregateReconciler[T]) Reconcile(ctx context.Context, req Request) (Re ctx = StashOriginalResourceType(ctx, r.Type) ctx = StashResourceType(ctx, r.Type) + beforeCtx, result, err := r.BeforeReconcile(ctx, req) + if err != nil { + return result, err + } + if beforeCtx != nil { + ctx = beforeCtx + } + resource := r.Type.DeepCopyObject().(T) if err := c.Get(ctx, req.NamespacedName, resource); err != nil { if apierrs.IsNotFound(err) { @@ -241,18 +275,19 @@ func (r *AggregateReconciler[T]) Reconcile(ctx context.Context, req Request) (Re if !errors.Is(err, ErrQuiet) { log.Error(err, "unable to fetch resource") } - return Result{}, err + return r.AfterReconcile(ctx, req, result, err) } } if resource.GetDeletionTimestamp() != nil { // resource is being deleted, nothing to do - return Result{}, nil + return r.AfterReconcile(ctx, req, result, nil) } - result, err := r.Reconciler.Reconcile(ctx, resource) + reconcileResult, err := r.Reconciler.Reconcile(ctx, resource) + result = AggregateResults(result, reconcileResult) if err != nil && !errors.Is(err, ErrHaltSubReconcilers) { - return result, err + return r.AfterReconcile(ctx, req, result, err) } // hack, ignore track requests from the child reconciler, we have it covered @@ -264,13 +299,13 @@ func (r *AggregateReconciler[T]) Reconcile(ctx context.Context, req Request) (Re }) desired, err := r.desiredResource(ctx, resource) if err != nil { - return Result{}, err + return r.AfterReconcile(ctx, req, result, err) } _, err = r.stamp.Manage(ctx, resource, resource, desired) if err != nil { - return Result{}, err + return r.AfterReconcile(ctx, req, result, err) } - return result, nil + return r.AfterReconcile(ctx, req, result, nil) } func (r *AggregateReconciler[T]) desiredResource(ctx context.Context, resource T) (T, error) { diff --git a/reconcilers/resource.go b/reconcilers/resource.go index 956fd2e..3573a45 100644 --- a/reconcilers/resource.go +++ b/reconcilers/resource.go @@ -83,6 +83,22 @@ type ResourceReconciler[Type client.Object] struct { // returned. Reconciler SubReconciler[Type] + // BeforeReconcile is called first thing for each reconcile request. A modified context may be + // returned. Errors are returned immediately. + // + // If BeforeReconcile is not defined, there is no effect. + // + // +optional + BeforeReconcile func(ctx context.Context, req Request) (context.Context, Result, error) + + // AfterReconcile is called following all work for the reconcile request. The result and error + // are provided and may be modified before returning. + // + // If AfterReconcile is not defined, the result and error are returned directly. + // + // +optional + AfterReconcile func(ctx context.Context, req Request, res Result, err error) (Result, error) + Config Config lazyInit sync.Once @@ -97,6 +113,16 @@ func (r *ResourceReconciler[T]) init() { if r.Name == "" { r.Name = fmt.Sprintf("%sResourceReconciler", typeName(r.Type)) } + if r.BeforeReconcile == nil { + r.BeforeReconcile = func(ctx context.Context, req reconcile.Request) (context.Context, reconcile.Result, error) { + return ctx, Result{}, nil + } + } + if r.AfterReconcile == nil { + r.AfterReconcile = func(ctx context.Context, req reconcile.Request, res reconcile.Result, err error) (reconcile.Result, error) { + return res, err + } + } }) } @@ -210,17 +236,26 @@ func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Res ctx = StashResourceType(ctx, r.Type) originalResource := r.Type.DeepCopyObject().(T) + beforeCtx, result, err := r.BeforeReconcile(ctx, req) + if err != nil { + return result, err + } + if beforeCtx != nil { + ctx = beforeCtx + } + if err := c.Get(ctx, req.NamespacedName, originalResource); err != nil { if apierrs.IsNotFound(err) { // we'll ignore not-found errors, since they can't be fixed by an immediate // requeue (we'll need to wait for a new notification), and we can get them // on deleted requests. - return Result{}, nil + return r.AfterReconcile(ctx, req, result, nil) } if !errors.Is(err, ErrQuiet) { log.Error(err, "unable to fetch resource") } - return Result{}, err + + return r.AfterReconcile(ctx, req, result, err) } resource := originalResource.DeepCopyObject().(T) @@ -230,10 +265,11 @@ func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Res } r.initializeConditions(ctx, resource) - result, err := r.reconcile(ctx, resource) + reconcileResult, err := r.reconcile(ctx, resource) + result = AggregateResults(result, reconcileResult) if r.SkipStatusUpdate { - return result, err + return r.AfterReconcile(ctx, req, result, err) } // attempt to restore last transition time for unchanged conditions @@ -251,7 +287,8 @@ func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Res c.Recorder.Eventf(resource, corev1.EventTypeWarning, "StatusPatchFailed", "Failed to patch status: %v", patchErr) } - return Result{}, patchErr + + return r.AfterReconcile(ctx, req, result, patchErr) } c.Recorder.Eventf(resource, corev1.EventTypeNormal, "StatusPatched", "Patched status") @@ -264,7 +301,7 @@ func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Res c.Recorder.Eventf(resource, corev1.EventTypeWarning, "StatusUpdateFailed", "Failed to update status: %v", updateErr) } - return Result{}, updateErr + return r.AfterReconcile(ctx, req, result, updateErr) } c.Recorder.Eventf(resource, corev1.EventTypeNormal, "StatusUpdated", "Updated status") diff --git a/reconcilers/webhook.go b/reconcilers/webhook.go index 843e5ed..900f6fe 100644 --- a/reconcilers/webhook.go +++ b/reconcilers/webhook.go @@ -74,6 +74,22 @@ type AdmissionWebhookAdapter[Type client.Object] struct { // Typically, Reconciler is a Sequence of multiple SubReconcilers. Reconciler SubReconciler[Type] + // BeforeHandle is called first thing for each admission request. A modified context may be + // returned. + // + // If BeforeHandle is not defined, there is no effect. + // + // +optional + BeforeHandle func(ctx context.Context, req admission.Request, resp *admission.Response) context.Context + + // AfterHandle is called following all work for the admission request. The response is provided + // and may be modified before returning. + // + // If AfterHandle is not defined, the response is returned directly. + // + // +optional + AfterHandle func(ctx context.Context, req admission.Request, resp *admission.Response) + Config Config lazyInit sync.Once @@ -88,6 +104,14 @@ func (r *AdmissionWebhookAdapter[T]) init() { if r.Name == "" { r.Name = fmt.Sprintf("%sAdmissionWebhookAdapter", typeName(r.Type)) } + if r.BeforeHandle == nil { + r.BeforeHandle = func(ctx context.Context, req admission.Request, resp *admission.Response) context.Context { + return ctx + } + } + if r.AfterHandle == nil { + r.AfterHandle = func(ctx context.Context, req admission.Request, resp *admission.Response) {} + } }) } @@ -142,6 +166,10 @@ func (r *AdmissionWebhookAdapter[T]) Handle(ctx context.Context, req admission.R ctx = StashAdmissionRequest(ctx, req) ctx = StashAdmissionResponse(ctx, resp) + if beforeCtx := r.BeforeHandle(ctx, req, resp); beforeCtx != nil { + ctx = beforeCtx + } + // defined for compatibility since this is not a reconciler ctx = StashRequest(ctx, Request{ NamespacedName: types.NamespacedName{ @@ -163,6 +191,7 @@ func (r *AdmissionWebhookAdapter[T]) Handle(ctx context.Context, req admission.R } } + r.AfterHandle(ctx, req, resp) return *resp }