Skip to content

Commit

Permalink
Merge pull request #502 from scothis/aspect-oriented-reconciler
Browse files Browse the repository at this point in the history
Advice reconciler
  • Loading branch information
scothis authored May 28, 2024
2 parents 7e41cef + ec7561c commit 597a9ed
Show file tree
Hide file tree
Showing 9 changed files with 928 additions and 35 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Within an existing Kubebuilder or controller-runtime project, reconcilers.io may
- [Higher-order Reconcilers](#higher-order-reconcilers)
- [CastResource](#castresource)
- [Sequence](#sequence)
- [Advice](#advice)
- [IfThen](#ifthen)
- [While](#while)
- [TryCatch](#trycatch)
Expand Down Expand Up @@ -457,6 +458,33 @@ func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler[*b
```
[full source](https://github.com/projectriff/system/blob/4c3b75327bf99cc37b57ba14df4c65d21dc79d28/pkg/controllers/build/function_reconciler.go#L39-L51)

#### Advice

[`Advice`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#Advice) is a sub reconciler for advising the lifecycle of another sub reconciler in an aspect oriented programming (AOP) style. `Before` is called before the delegated reconciler and `After` afterward. `Around` is used between Before and After to have full control over how the delegated reconciler is called, including suppressing the call, modifying the input or result, or calling the reconciler multiple times.

**Example:**

Advice can be used to control calls to a reconciler at a lower level. In this case the reconciler is called twice aggregating the results while returning immediately on error.

```go
func CallTwice(reconciler reconciler.SubReconciler[*buildv1alpha1.Function]) *reconcilers.SubReconciler[*buildv1alpha1.Function] {
return &reconcilers.Advice[*buildv1alpha1.Function]{
Reconciler: reconciler,
Around: func(ctx context.Context, resource *resources.TestResource, reconciler reconcilers.SubReconciler[*resources.TestResource]) (reconcile.Result, error) {
result := reconcilers.Result{}
for i := 0; i < 2; i++ {
if r, err := reconciler.Reconcile(ctx, resource); true {
result = reconcilers.AggregateResults(result, r)
} else if err != nil {
return result, err
}
}
return result, nil
},
}
}
```

#### IfThen

An [`IfThen`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#IfThen) branches execution of the current reconcile request based on a condition. The false `Else` branch is optional and ignored if not defined.
Expand Down
155 changes: 155 additions & 0 deletions reconcilers/advice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
Copyright 2024 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package reconcilers

import (
"context"
"fmt"
"sync"

"github.com/go-logr/logr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
_ SubReconciler[client.Object] = (*Advice[client.Object])(nil)
)

// Advice is a sub reconciler for advising the lifecycle of another sub reconciler in an aspect
// oriented programming style.
type Advice[Type client.Object] struct {
// Name used to identify this reconciler. Defaults to `Advice`. Ideally unique, but
// not required to be so.
//
// +optional
Name string

// Setup performs initialization on the manager and builder this reconciler
// will run with. It's common to setup field indexes and watch resources.
//
// +optional
Setup func(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error

// Reconciler being advised
Reconciler SubReconciler[Type]

// Before is called preceding Around. A modified context may be returned. Errors are returned
// immediately.
//
// If Before is not defined, there is no effect.
//
// +optional
Before func(ctx context.Context, resource Type) (context.Context, Result, error)

// Around is responsible for invoking the reconciler and returning the result. Implementations
// may choose to not invoke the reconciler, invoke a different reconciler or invoke the
// reconciler multiple times.
//
// If Around is not defined, the Reconciler is invoked once.
//
// +optional
Around func(ctx context.Context, resource Type, reconciler SubReconciler[Type]) (Result, error)

// After is called following Around. The result and error from Around are provided and may be
// modified before returning.
//
// If After is not defined, the result and error are returned directly.
//
// +optional
After func(ctx context.Context, resource Type, result Result, err error) (Result, error)

lazyInit sync.Once
}

func (r *Advice[T]) init() {
r.lazyInit.Do(func() {
if r.Name == "" {
r.Name = "Advice"
}
if r.Before == nil {
r.Before = func(ctx context.Context, resource T) (context.Context, Result, error) {
return nil, Result{}, nil
}
}
if r.Around == nil {
r.Around = func(ctx context.Context, resource T, reconciler SubReconciler[T]) (Result, error) {
return reconciler.Reconcile(ctx, resource)
}
}
if r.After == nil {
r.After = func(ctx context.Context, resource T, result Result, err error) (Result, error) {
return result, err
}
}
})
}

func (r *Advice[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error {
r.init()

log := logr.FromContextOrDiscard(ctx).
WithName(r.Name)
ctx = logr.NewContext(ctx, log)

if r.Setup == nil {
return nil
}
if err := r.validate(ctx); err != nil {
return err
}
if err := r.Setup(ctx, mgr, bldr); err != nil {
return err
}
return r.Reconciler.SetupWithManager(ctx, mgr, bldr)
}

func (r *Advice[T]) validate(ctx context.Context) error {
if r.Reconciler == nil {
return fmt.Errorf("Advice %q must implement Reconciler", r.Name)
}
if r.Before == nil && r.Around == nil && r.After == nil {
return fmt.Errorf("Advice %q must implement at least one of Before, Around or After", r.Name)
}

return nil
}

func (r *Advice[T]) Reconcile(ctx context.Context, resource T) (Result, error) {
r.init()

log := logr.FromContextOrDiscard(ctx).
WithName(r.Name)
ctx = logr.NewContext(ctx, log)

// before phase
beforeCtx, result, err := r.Before(ctx, resource)
if err != nil {
return result, err
}
if beforeCtx != nil {
ctx = beforeCtx
}

// around phase
aroundResult, err := r.Around(ctx, resource, r.Reconciler)
result = AggregateResults(result, aroundResult)

// after phase
return r.After(ctx, resource, result, err)
}
Loading

0 comments on commit 597a9ed

Please sign in to comment.