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

Cast parent resource type #51

Merged
merged 3 commits into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, common referred to as a duck type.
scothis marked this conversation as resolved.
Show resolved Hide resolved

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.
scothis marked this conversation as resolved.
Show resolved Hide resolved

**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, common referred to as a duck type.
scothis marked this conversation as resolved.
Show resolved Hide resolved
//
// 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