Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

Commit

Permalink
Cast parent resource type (#51)
Browse files Browse the repository at this point in the history
A CastParent reconciler casts the ParentReconciler's type by projecting the
resource data onto a new struct. Casting the parent resource is
useful to create cross cutting reconcilers that can operate on common
portion of multiple parent resources, commonly referred to as a duck type.

JSON encoding is used as the intermediate representation. Operations on
a cast parent are read-only. Attempts to mutate the parent will result
in the reconciler erring.

```
func FunctionReconciler(c reconcilers.Config) *reconcilers.ParentReconciler {
	c.Log = c.Log.WithName("Function")

	return &reconcilers.ParentReconciler{
		Type: &buildv1alpha1.Function{},
		Reconciler: reconcilers.Sequence{
			&reconcilers.CastParent{
				Type: &duckv1alpha1.ImageRef{},
				Reconciler: &reconcilers.SyncReconciler{
					Sync: func(ctx context.Context, parent *duckv1alpha1.ImageRef) error {
						// do something with the duckv1alpha1.ImageRef instead of a buildv1alpha1.Function
						return nil
					},
					Config: c,
				},
			},
			FunctionChildImageReconciler(c),
		},

		Config: c,
	}
}
```

Signed-off-by: Scott Andrews <[email protected]>
Co-authored-by: Glyn Normington <[email protected]>
  • Loading branch information
scothis and glyn authored Dec 7, 2020
1 parent 2db5e1f commit c3f7a25
Show file tree
Hide file tree
Showing 7 changed files with 429 additions and 11 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
- [SubReconciler](#subreconciler)
- [SyncReconciler](#syncreconciler)
- [ChildReconciler](#childreconciler)
- [Higher-order Reconcilers](#higher-order-reconcilers)
- [CastParent](#castparent)
- [Sequence](#sequence)
- [Testing](#testing)
- [ReconcilerTestSuite](#reconcilertestsuite)
Expand Down Expand Up @@ -197,6 +199,43 @@ func FunctionChildImageReconciler(c reconcilers.Config) reconcilers.SubReconcile
```
[full source](https://github.com/projectriff/system/blob/1fcdb7a090565d6750f9284a176eb00a3fe14663/pkg/controllers/build/function_reconciler.go#L76-L151)

### Higher-order Reconcilers

Higher order reconcilers are SubReconcilers that do not perform work directly, but instead compose other SubReconcilers in new patterns.

#### CastParent

A [`CastParent`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#CastParent) casts the ParentReconciler's type by projecting the resource data onto a new struct. Casting the parent resource is useful to create cross cutting reconcilers that can operate on common portion of multiple parent resources, commonly referred to as a duck type.

JSON encoding is used as the intermediate representation. Operations on a cast parent are read-only. Attempts to mutate the parent will result in the reconciler erring, although read/write support may be added in the future.

**Example:**

```go
func FunctionReconciler(c reconcilers.Config) *reconcilers.ParentReconciler {
c.Log = c.Log.WithName("Function")

return &reconcilers.ParentReconciler{
Type: &buildv1alpha1.Function{},
Reconciler: reconcilers.Sequence{
&reconcilers.CastParent{
Type: &duckv1alpha1.ImageRef{},
Reconciler: &reconcilers.SyncReconciler{
Sync: func(ctx context.Context, parent *duckv1alpha1.ImageRef) error {
// do something with the duckv1alpha1.ImageRef instead of a buildv1alpha1.Function
return nil
},
Config: c,
},
},
FunctionChildImageReconciler(c),
},

Config: c,
}
}
```

#### Sequence

A [`Sequence`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#Sequence) composes multiple SubReconcilers as a single SubReconciler. Each sub reconciler is called in turn, aggregating the result of each sub reconciler. A reconciler returning an error will interrupt the sequence.
Expand Down
103 changes: 95 additions & 8 deletions reconcilers/reconcilers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package reconcilers

import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -81,6 +82,7 @@ type ParentReconciler struct {

func (r *ParentReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
ctx = StashParentType(ctx, r.Type)
ctx = StashCastParentType(ctx, r.Type)
bldr := ctrl.NewControllerManagedBy(mgr).For(r.Type)
if err := r.Reconciler.SetupWithManager(ctx, mgr, bldr); err != nil {
return err
Expand All @@ -93,6 +95,7 @@ func (r *ParentReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("request", req.NamespacedName)

ctx = StashParentType(ctx, r.Type)
ctx = StashCastParentType(ctx, r.Type)
originalParent := r.Type.DeepCopyObject().(apis.Object)

if err := r.Get(ctx, req.NamespacedName, originalParent); err != nil {
Expand Down Expand Up @@ -163,11 +166,16 @@ func (r *ParentReconciler) status(obj apis.Object) interface{} {
}

const parentTypeStashKey StashKey = "reconciler-runtime:parentType"
const castParentTypeStashKey StashKey = "reconciler-runtime:castParentType"

func StashParentType(ctx context.Context, parentType runtime.Object) context.Context {
return context.WithValue(ctx, parentTypeStashKey, parentType)
}

func StashCastParentType(ctx context.Context, currentType runtime.Object) context.Context {
return context.WithValue(ctx, castParentTypeStashKey, currentType)
}

func RetrieveParentType(ctx context.Context) runtime.Object {
value := ctx.Value(parentTypeStashKey)
if parentType, ok := value.(runtime.Object); ok {
Expand All @@ -176,6 +184,14 @@ func RetrieveParentType(ctx context.Context) runtime.Object {
return nil
}

func RetrieveCastParentType(ctx context.Context) runtime.Object {
value := ctx.Value(castParentTypeStashKey)
if currentType, ok := value.(runtime.Object); ok {
return currentType
}
return nil
}

// SubReconciler are participants in a larger reconciler request. The resource
// being reconciled is passed directly to the sub reconciler. The resource's
// status can be mutated to reflect the current state.
Expand All @@ -188,6 +204,7 @@ var (
_ SubReconciler = (*SyncReconciler)(nil)
_ SubReconciler = (*ChildReconciler)(nil)
_ SubReconciler = (Sequence)(nil)
_ SubReconciler = (*CastParent)(nil)
)

// SyncReconciler is a sub reconciler for custom reconciliation logic. No
Expand Down Expand Up @@ -226,12 +243,12 @@ func (r *SyncReconciler) validate(ctx context.Context) error {
if r.Sync == nil {
return fmt.Errorf("SyncReconciler must implement Sync")
} else {
parentType := RetrieveParentType(ctx)
castParentType := RetrieveCastParentType(ctx)
fn := reflect.TypeOf(r.Sync)
err := fmt.Errorf("SyncReconciler must implement Sync: func(context.Context, %s) error | func(context.Context, %s) (ctrl.Result, error), found: %s", reflect.TypeOf(parentType), reflect.TypeOf(parentType), fn)
err := fmt.Errorf("SyncReconciler must implement Sync: func(context.Context, %s) error | func(context.Context, %s) (ctrl.Result, error), found: %s", reflect.TypeOf(castParentType), reflect.TypeOf(castParentType), fn)
if fn.NumIn() != 2 ||
!reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) ||
!reflect.TypeOf(parentType).AssignableTo(fn.In(1)) {
!reflect.TypeOf(castParentType).AssignableTo(fn.In(1)) {
return err
}
switch fn.NumOut() {
Expand Down Expand Up @@ -406,7 +423,7 @@ func (r *ChildReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager
}

func (r *ChildReconciler) validate(ctx context.Context) error {
parentType := RetrieveParentType(ctx)
castParentType := RetrieveCastParentType(ctx)

// validate IndexField value
if r.IndexField == "" {
Expand All @@ -431,10 +448,10 @@ func (r *ChildReconciler) validate(ctx context.Context) error {
fn := reflect.TypeOf(r.DesiredChild)
if fn.NumIn() != 2 || fn.NumOut() != 2 ||
!reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) ||
!reflect.TypeOf(parentType).AssignableTo(fn.In(1)) ||
!reflect.TypeOf(castParentType).AssignableTo(fn.In(1)) ||
!reflect.TypeOf(r.ChildType).AssignableTo(fn.Out(0)) ||
!reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(1)) {
return fmt.Errorf("ChildReconciler must implement DesiredChild: func(context.Context, %s) (%s, error), found: %s", reflect.TypeOf(parentType), reflect.TypeOf(r.ChildType), fn)
return fmt.Errorf("ChildReconciler must implement DesiredChild: func(context.Context, %s) (%s, error), found: %s", reflect.TypeOf(castParentType), reflect.TypeOf(r.ChildType), fn)
}
}

Expand All @@ -445,10 +462,10 @@ func (r *ChildReconciler) validate(ctx context.Context) error {
} else {
fn := reflect.TypeOf(r.ReflectChildStatusOnParent)
if fn.NumIn() != 3 || fn.NumOut() != 0 ||
!reflect.TypeOf(parentType).AssignableTo(fn.In(0)) ||
!reflect.TypeOf(castParentType).AssignableTo(fn.In(0)) ||
!reflect.TypeOf(r.ChildType).AssignableTo(fn.In(1)) ||
!reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.In(2)) {
return fmt.Errorf("ChildReconciler must implement ReflectChildStatusOnParent: func(%s, %s, error), found: %s", reflect.TypeOf(parentType), reflect.TypeOf(r.ChildType), fn)
return fmt.Errorf("ChildReconciler must implement ReflectChildStatusOnParent: func(%s, %s, error), found: %s", reflect.TypeOf(castParentType), reflect.TypeOf(r.ChildType), fn)
}
}

Expand Down Expand Up @@ -780,6 +797,76 @@ func (r Sequence) aggregateResult(result, aggregate ctrl.Result) ctrl.Result {
return aggregate
}

// CastParent casts the ParentReconciler's type by projecting the resource data
// onto a new struct. Casting the parent resource is useful to create cross
// cutting reconcilers that can operate on common portion of multiple parent
// resources, commonly referred to as a duck type.
//
// JSON encoding is used as the intermediate representation. Operations on a
// cast parent are read-only. Attempts to mutate the parent will result in the
// reconciler erring.
type CastParent struct {
// Type of resource to reconcile
Type runtime.Object

// Reconciler is called for each reconciler request with the parent
// resource being reconciled. Typically a Sequence is used to compose
// multiple SubReconcilers.
Reconciler SubReconciler
}

func (r *CastParent) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error {
if err := r.validate(ctx); err != nil {
return err
}
return r.Reconciler.SetupWithManager(ctx, mgr, bldr)
}

func (r *CastParent) validate(ctx context.Context) error {
// validate Type value
if r.Type == nil {
return fmt.Errorf("Type must be defined")
}

// validate Reconciler value
if r.Reconciler == nil {
return fmt.Errorf("Reconciler must be defined")
}

return nil
}

func (r *CastParent) Reconcile(ctx context.Context, parent apis.Object) (ctrl.Result, error) {
ctx, castParent, err := r.cast(ctx, parent)
if err != nil {
return ctrl.Result{}, err
}
castOriginal := castParent.DeepCopyObject()
result, err := r.Reconciler.Reconcile(ctx, castParent)
if err != nil {
return ctrl.Result{}, err
}
if !equality.Semantic.DeepEqual(castParent, castOriginal) {
// TODO apply diff to parent resource, until then err
return ctrl.Result{}, fmt.Errorf("cast parent resource mutated")
}
return result, nil
}

func (r *CastParent) cast(ctx context.Context, parent runtime.Object) (context.Context, apis.Object, error) {
data, err := json.Marshal(parent)
if err != nil {
return nil, nil, err
}
castParent := r.Type.DeepCopyObject().(apis.Object)
err = json.Unmarshal(data, castParent)
if err != nil {
return nil, nil, err
}
ctx = StashCastParentType(ctx, castParent)
return ctx, castParent, nil
}

func typeName(i interface{}) string {
t := reflect.TypeOf(i)
// TODO do we need this?
Expand Down
Loading

0 comments on commit c3f7a25

Please sign in to comment.