From b8fd9d1f1e8801e33142ef18e93fed9ba99dbfa3 Mon Sep 17 00:00:00 2001 From: Glen Rodgers Date: Wed, 6 Apr 2022 21:09:07 +0000 Subject: [PATCH 1/4] Add optional Finalize to ParentReconciler. - Used to clean up unowned resources on a delete reconcile. --- README.md | 2 + reconcilers/reconcilers.go | 78 ++++++++++- reconcilers/reconcilers_test.go | 159 ++++++++++++++++++++++- reconcilers/reconcilers_validate_test.go | 74 +++++++++++ 4 files changed, 311 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9c11a7..0e33744 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,11 @@ The parent is responsible for: - updates the resource status if it was modified - logging the reconcilers activities - records events for mutations and errors +- adding and removing a finalizer if needed The implementor is responsible for: - defining the set of sub reconcilers +- providing instructions on how to clean up on deletion if needed **Example:** diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go index 3e75975..1e0ff5d 100644 --- a/reconcilers/reconcilers.go +++ b/reconcilers/reconcilers.go @@ -25,6 +25,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -70,9 +71,18 @@ type ParentReconciler struct { // Reconciler is called for each reconciler request with the parent // resource being reconciled. Typically, Reconciler is a Sequence of - // multiple SubReconcilers. + // multiple SubReconcilers. It is not called on a delete request. Reconciler SubReconciler + // Finalize is called on a delete request. Typically, Finalize cleans + // up any state, created by previous reconciles, of which the parent is + // not a Kubernetes owner. + // + // Expected function signature: + // func(ctx context.Context, parent client.Object) error + // +optional + Finalize interface{} + Config } @@ -83,9 +93,32 @@ func (r *ParentReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manage if err := r.Reconciler.SetupWithManager(ctx, mgr, bldr); err != nil { return err } + + if err := r.validate(ctx); err != nil { + return err + } + return bldr.Complete(r) } +func (r *ParentReconciler) validate(ctx context.Context) error { + castParentType := RetrieveCastParentType(ctx) + // validate Finalize function signature: + // nil + // func(ctx context.Context, parent client.Object) error + if r.Finalize != nil { + fn := reflect.TypeOf(r.Finalize) + if fn.NumIn() != 2 || fn.NumOut() != 1 || + !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || + !reflect.TypeOf(castParentType).AssignableTo(fn.In(1)) || + !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(0)) { + return fmt.Errorf("ParentReconciler Finalize must have correct signature: func(context.Context, %s) error, found: %s", reflect.TypeOf(castParentType), fn) + } + } + + return nil +} + func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ctx = WithStash(ctx) log := r.Log.WithValues("request", req.NamespacedName) @@ -117,6 +150,7 @@ func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr initializeConditions.Call([]reflect.Value{}) } } + result, err := r.reconcile(ctx, parent) if r.hasStatus(originalParent) { @@ -137,12 +171,41 @@ func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr "Updated status") } } + + if !equality.Semantic.DeepEqual(parent.GetFinalizers(), originalParent.GetFinalizers()) { + log.Info("updating finalizers", "diff", cmp.Diff(parent.GetFinalizers(), originalParent.GetFinalizers())) + if updateErr := r.Update(ctx, parent); updateErr != nil { + log.Error(updateErr, "unable to update", typeName(r.Type), parent) + r.Recorder.Eventf(parent, corev1.EventTypeWarning, "UpdateFailed", + "Failed to update: %v", updateErr) + return ctrl.Result{}, updateErr + } + r.Recorder.Eventf(parent, corev1.EventTypeNormal, "Updated", "Updated") + } + // return original reconcile result return result, err } func (r *ParentReconciler) reconcile(ctx context.Context, parent client.Object) (ctrl.Result, error) { + finalizer := fmt.Sprintf("%s/reconciler-runtime-finalize", parent.GetObjectKind().GroupVersionKind().Group) + if r.Finalize == nil { + controllerutil.RemoveFinalizer(parent, finalizer) + } else { + controllerutil.AddFinalizer(parent, finalizer) + } + + if r.Finalize == nil && parent.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + if parent.GetDeletionTimestamp() != nil { + if err := r.finalize(ctx, parent); err != nil { + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(parent, finalizer) + return ctrl.Result{}, nil } @@ -154,6 +217,19 @@ func (r *ParentReconciler) reconcile(ctx context.Context, parent client.Object) r.copyGeneration(parent) return result, nil } + +func (r *ParentReconciler) finalize(ctx context.Context, parent client.Object) error { + fn := reflect.ValueOf(r.Finalize) + out := fn.Call([]reflect.Value{ + reflect.ValueOf(ctx), + reflect.ValueOf(parent), + }) + if !out[0].IsNil() { + return out[0].Interface().(error) + } + return nil +} + func (r *ParentReconciler) hasStatus(obj client.Object) bool { return reflect.ValueOf(obj).Elem().FieldByName("Status").IsValid() } diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index 63c6c48..66edb32 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -95,6 +95,7 @@ func TestParentReconciler(t *testing.T) { ) }) deletedAt := metav1.NewTime(time.UnixMilli(2000)) + const finalizer = "testing.reconciler.runtime/reconciler-runtime-finalize" rts := rtesting.ReconcilerTestSuite{{ Name: "resource does not exist", @@ -302,14 +303,170 @@ func TestParentReconciler(t *testing.T) { } }, }, + }, { + Name: "has Finalize", + Key: testKey, + GivenObjects: []client.Object{ + resource, + }, + ExpectUpdates: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers(finalizer) + }).SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("Defaulter", "ran") + }), + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.Sequence{} + }, + "Finalize": func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + }, + }, { + Name: "has finalizer", + Key: testKey, + GivenObjects: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers(finalizer) + }), + }, + ExpectUpdates: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers() + }).SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("Defaulter", "ran") + }), + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.Sequence{} + }, + }, + }, { + Name: "has Finalize and has finalizer", + Key: testKey, + GivenObjects: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers(finalizer) + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.Sequence{} + }, + "Finalize": func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + }, + }, { + Name: "has finalize, has finalizer, and is deleted", + Key: testKey, + GivenObjects: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletedAt) + d.Finalizers(finalizer) + }), + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + }, + ExpectUpdates: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletedAt) + d.Finalizers() + }).SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("Defaulter", "ran") + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.Sequence{} + }, + "Finalize": func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + }, + }, { + Name: "Finalize erred", + Key: testKey, + GivenObjects: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletedAt) + d.Finalizers(finalizer) + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.Sequence{} + }, + "Finalize": func(ctx context.Context, parent *resources.TestResource) error { + return fmt.Errorf("Finalize error") + }, + }, + ShouldErr: true, + }, { + Name: "has Finalize and status", + Key: testKey, + GivenObjects: []client.Object{ + resource, + }, + ExpectUpdates: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers(finalizer) + }).SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("Defaulter", "ran") + }).StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("Reconciler", "ran") + }), + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "StatusUpdated", + `Updated status`), + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + }, + ExpectStatusUpdates: []client.Object{ + resource.StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("Reconciler", "ran") + }), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + if parent.Status.Fields == nil { + parent.Status.Fields = map[string]string{} + } + parent.Status.Fields["Reconciler"] = "ran" + return nil + }, + } + }, + "Finalize": func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + }, }} rts.Test(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - return &reconcilers.ParentReconciler{ + reconciler := &reconcilers.ParentReconciler{ Type: &resources.TestResource{}, Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), Config: c, } + if _, ok := rtc.Metadata["Finalize"]; ok { + reconciler.Finalize = rtc.Metadata["Finalize"].(func(context.Context, *resources.TestResource) error) + } + + return reconciler }) } diff --git a/reconcilers/reconcilers_validate_test.go b/reconcilers/reconcilers_validate_test.go index d6db56d..00255cf 100644 --- a/reconcilers/reconcilers_validate_test.go +++ b/reconcilers/reconcilers_validate_test.go @@ -14,6 +14,80 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +func TestParentReconciler_validate(t *testing.T) { + tests := []struct { + name string + parent client.Object + reconciler *ParentReconciler + shouldErr string + }{ + { + name: "empty", + parent: &corev1.ConfigMap{}, + reconciler: &ParentReconciler{}, + }, + { + name: "valid", + parent: &corev1.ConfigMap{}, + reconciler: &ParentReconciler{ + Finalize: func(ctx context.Context, parent *corev1.ConfigMap) error { + return nil + }, + }, + }, + { + name: "Finalize num in", + parent: &corev1.ConfigMap{}, + reconciler: &ParentReconciler{ + Finalize: func() error { + return nil + }, + }, + shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func() error", + }, + { + name: "Finalize in 1", + parent: &corev1.ConfigMap{}, + reconciler: &ParentReconciler{ + Finalize: func(ctx context.Context, parent *corev1.Secret) error { + return nil + }, + }, + shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.Secret) error", + }, + { + name: "Finalize num out", + parent: &corev1.ConfigMap{}, + reconciler: &ParentReconciler{ + Finalize: func(ctx context.Context, parent *corev1.ConfigMap) (ctrl.Result, error) { + return ctrl.Result{}, nil + }, + }, + shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.ConfigMap) (reconcile.Result, error)", + }, + { + name: "Finalize out 1", + parent: &corev1.ConfigMap{}, + reconciler: &ParentReconciler{ + Finalize: func(ctx context.Context, parent *corev1.ConfigMap) *ctrl.Result { + return nil + }, + }, + shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.ConfigMap) *reconcile.Result", + }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + ctx := StashCastParentType(context.TODO(), c.parent) + err := c.reconciler.validate(ctx) + if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) { + t.Errorf("validate() error = %q, shouldErr %q", err, c.shouldErr) + } + }) + } +} + func TestSyncReconciler_validate(t *testing.T) { tests := []struct { name string From e692211f30cfb8a1785977ca6d713677819db9bd Mon Sep 17 00:00:00 2001 From: Glen Rodgers Date: Thu, 7 Apr 2022 10:21:12 -0400 Subject: [PATCH 2/4] Be specific in finalizer update err and events. Co-authored-by: Scott Andrews --- reconcilers/reconcilers.go | 6 +++--- reconcilers/reconcilers_test.go | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go index 1e0ff5d..6920077 100644 --- a/reconcilers/reconcilers.go +++ b/reconcilers/reconcilers.go @@ -177,10 +177,10 @@ func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if updateErr := r.Update(ctx, parent); updateErr != nil { log.Error(updateErr, "unable to update", typeName(r.Type), parent) r.Recorder.Eventf(parent, corev1.EventTypeWarning, "UpdateFailed", - "Failed to update: %v", updateErr) + "Failed to update finalizers: %v", updateErr) return ctrl.Result{}, updateErr } - r.Recorder.Eventf(parent, corev1.EventTypeNormal, "Updated", "Updated") + r.Recorder.Eventf(parent, corev1.EventTypeNormal, "Updated", "Updated finalizers") } // return original reconcile result @@ -188,7 +188,7 @@ func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } func (r *ParentReconciler) reconcile(ctx context.Context, parent client.Object) (ctrl.Result, error) { - finalizer := fmt.Sprintf("%s/reconciler-runtime-finalize", parent.GetObjectKind().GroupVersionKind().Group) + finalizer := fmt.Sprintf("%s/reconciler-runtime-finalizer", parent.GetObjectKind().GroupVersionKind().Group) if r.Finalize == nil { controllerutil.RemoveFinalizer(parent, finalizer) } else { diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index 66edb32..eac0a60 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -95,7 +95,7 @@ func TestParentReconciler(t *testing.T) { ) }) deletedAt := metav1.NewTime(time.UnixMilli(2000)) - const finalizer = "testing.reconciler.runtime/reconciler-runtime-finalize" + const finalizer = "testing.reconciler.runtime/reconciler-runtime-finalizer" rts := rtesting.ReconcilerTestSuite{{ Name: "resource does not exist", @@ -317,7 +317,7 @@ func TestParentReconciler(t *testing.T) { }), }, ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), }, Metadata: map[string]interface{}{ "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { @@ -343,7 +343,7 @@ func TestParentReconciler(t *testing.T) { }), }, ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), }, Metadata: map[string]interface{}{ "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { @@ -376,7 +376,7 @@ func TestParentReconciler(t *testing.T) { }), }, ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), }, ExpectUpdates: []client.Object{ resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -430,7 +430,7 @@ func TestParentReconciler(t *testing.T) { ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "StatusUpdated", `Updated status`), - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated`), + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), }, ExpectStatusUpdates: []client.Object{ resource.StatusDie(func(d *dies.TestResourceStatusDie) { From 47f6d8beaf555e76bece95ad024ea37c69015a51 Mon Sep 17 00:00:00 2001 From: Glen Rodgers Date: Fri, 8 Apr 2022 14:35:55 +0000 Subject: [PATCH 3/4] Add test case for finalizer update erroring. --- reconcilers/reconcilers_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index eac0a60..d4b112b 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -454,6 +454,34 @@ func TestParentReconciler(t *testing.T) { return nil }, }, + }, { + Name: "finalizers update failed", + Key: testKey, + GivenObjects: []client.Object{ + resource, + }, + WithReactors: []rtesting.ReactionFunc{ + rtesting.InduceFailure("update", "TestResource"), + }, + ExpectUpdates: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers(finalizer) + }).SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("Defaulter", "ran") + }), + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeWarning, "UpdateFailed", `Failed to update finalizers: inducing failure for update TestResource`), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.Sequence{} + }, + "Finalize": func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + }, + ShouldErr: true, }} rts.Test(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { From a2b39066f7f883b3d35adb4e884d96f47c5c21aa Mon Sep 17 00:00:00 2001 From: Glen Rodgers Date: Fri, 15 Apr 2022 14:38:45 +0000 Subject: [PATCH 4/4] Move Finalize method to SyncReconciler. - ParentReconciler allows delete reconciles - ChildReconciler ignores them - Each syncReconciler adds a unique finalizer (supplied via stash) --- reconcilers/reconcilers.go | 135 ++++----- reconcilers/reconcilers_test.go | 347 +++++++++++------------ reconcilers/reconcilers_validate_test.go | 134 ++++----- 3 files changed, 302 insertions(+), 314 deletions(-) diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go index 6920077..9acf509 100644 --- a/reconcilers/reconcilers.go +++ b/reconcilers/reconcilers.go @@ -74,15 +74,6 @@ type ParentReconciler struct { // multiple SubReconcilers. It is not called on a delete request. Reconciler SubReconciler - // Finalize is called on a delete request. Typically, Finalize cleans - // up any state, created by previous reconciles, of which the parent is - // not a Kubernetes owner. - // - // Expected function signature: - // func(ctx context.Context, parent client.Object) error - // +optional - Finalize interface{} - Config } @@ -94,31 +85,9 @@ func (r *ParentReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manage return err } - if err := r.validate(ctx); err != nil { - return err - } - return bldr.Complete(r) } -func (r *ParentReconciler) validate(ctx context.Context) error { - castParentType := RetrieveCastParentType(ctx) - // validate Finalize function signature: - // nil - // func(ctx context.Context, parent client.Object) error - if r.Finalize != nil { - fn := reflect.TypeOf(r.Finalize) - if fn.NumIn() != 2 || fn.NumOut() != 1 || - !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || - !reflect.TypeOf(castParentType).AssignableTo(fn.In(1)) || - !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(0)) { - return fmt.Errorf("ParentReconciler Finalize must have correct signature: func(context.Context, %s) error, found: %s", reflect.TypeOf(castParentType), fn) - } - } - - return nil -} - func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ctx = WithStash(ctx) log := r.Log.WithValues("request", req.NamespacedName) @@ -188,27 +157,6 @@ func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } func (r *ParentReconciler) reconcile(ctx context.Context, parent client.Object) (ctrl.Result, error) { - finalizer := fmt.Sprintf("%s/reconciler-runtime-finalizer", parent.GetObjectKind().GroupVersionKind().Group) - if r.Finalize == nil { - controllerutil.RemoveFinalizer(parent, finalizer) - } else { - controllerutil.AddFinalizer(parent, finalizer) - } - - if r.Finalize == nil && parent.GetDeletionTimestamp() != nil { - return ctrl.Result{}, nil - } - - if parent.GetDeletionTimestamp() != nil { - if err := r.finalize(ctx, parent); err != nil { - return ctrl.Result{}, err - } - - controllerutil.RemoveFinalizer(parent, finalizer) - - return ctrl.Result{}, nil - } - result, err := r.Reconciler.Reconcile(ctx, parent) if err != nil { return ctrl.Result{}, err @@ -218,18 +166,6 @@ func (r *ParentReconciler) reconcile(ctx context.Context, parent client.Object) return result, nil } -func (r *ParentReconciler) finalize(ctx context.Context, parent client.Object) error { - fn := reflect.ValueOf(r.Finalize) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(parent), - }) - if !out[0].IsNil() { - return out[0].Interface().(error) - } - return nil -} - func (r *ParentReconciler) hasStatus(obj client.Object) bool { return reflect.ValueOf(obj).Elem().FieldByName("Status").IsValid() } @@ -339,6 +275,15 @@ type SyncReconciler struct { // func(ctx context.Context, parent client.Object) (ctrl.Result, error) Sync interface{} + // Finalize is called on a delete request. Typically, Finalize cleans + // up any state, created by previous reconciles, of which the parent is + // not a Kubernetes owner. + // + // Expected function signature: + // func(ctx context.Context, parent client.Object) error + // +optional + Finalize interface{} + Config } @@ -382,10 +327,56 @@ func (r *SyncReconciler) validate(ctx context.Context) error { } } + // validate Finalize function signature: + // nil + // func(ctx context.Context, parent client.Object) error + if r.Finalize != nil { + castParentType := RetrieveCastParentType(ctx) + fn := reflect.TypeOf(r.Finalize) + if fn.NumIn() != 2 || fn.NumOut() != 1 || + !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || + !reflect.TypeOf(castParentType).AssignableTo(fn.In(1)) || + !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(0)) { + return fmt.Errorf("SyncReconciler Finalize must have correct signature: func(context.Context, %s) error, found: %s", reflect.TypeOf(castParentType), fn) + } + } + return nil } +func UniqueFinalizer(ctx context.Context, group string) string { + finalizerCount, ok := RetrieveValue(ctx, "finalizer-count").(int) + if !ok { + finalizerCount = 1 + } + finalizer := fmt.Sprintf("%s/%d-finalizer", group, finalizerCount) + StashValue(ctx, "finalizer-count", finalizerCount+1) + + return finalizer +} + func (r *SyncReconciler) Reconcile(ctx context.Context, parent client.Object) (ctrl.Result, error) { + finalizer := UniqueFinalizer(ctx, parent.GetObjectKind().GroupVersionKind().Group) + if r.Finalize != nil { + controllerutil.AddFinalizer(parent, finalizer) + } else { + controllerutil.RemoveFinalizer(parent, finalizer) + } + + if r.Finalize == nil && parent.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + + if parent.GetDeletionTimestamp() != nil { + if err := r.finalize(ctx, parent); err != nil { + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(parent, finalizer) + + return ctrl.Result{}, nil + } + result, err := r.sync(ctx, parent) if err != nil { r.Log.Error(err, "unable to sync", typeName(parent), parent) @@ -417,6 +408,18 @@ func (r *SyncReconciler) sync(ctx context.Context, parent client.Object) (ctrl.R return result, err } +func (r *SyncReconciler) finalize(ctx context.Context, parent client.Object) error { + fn := reflect.ValueOf(r.Finalize) + out := fn.Call([]reflect.Value{ + reflect.ValueOf(ctx), + reflect.ValueOf(parent), + }) + if !out[0].IsNil() { + return out[0].Interface().(error) + } + return nil +} + var ( OnlyReconcileChildStatus = errors.New("skip reconciler create/update/delete behavior for the child resource, while still reflecting the existing child's status on the parent") ) @@ -652,6 +655,10 @@ func (r *ChildReconciler) Reconcile(ctx context.Context, parent client.Object) ( r.mutationCache = cache.NewExpiring() } + if parent.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + child, err := r.reconcile(ctx, parent) if err != nil { parentType := RetrieveParentType(ctx) diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index d4b112b..a2ee8e2 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -95,7 +95,6 @@ func TestParentReconciler(t *testing.T) { ) }) deletedAt := metav1.NewTime(time.UnixMilli(2000)) - const finalizer = "testing.reconciler.runtime/reconciler-runtime-finalizer" rts := rtesting.ReconcilerTestSuite{{ Name: "resource does not exist", @@ -111,25 +110,6 @@ func TestParentReconciler(t *testing.T) { } }, }, - }, { - Name: "ignore deleted resource", - Key: testKey, - GivenObjects: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.DeletionTimestamp(&deletedAt) - }), - }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ - Config: c, - Sync: func(ctx context.Context, parent *resources.TestResource) error { - t.Error("should not be called") - return nil - }, - } - }, - }, }, { Name: "error fetching resource", Key: testKey, @@ -232,6 +212,36 @@ func TestParentReconciler(t *testing.T) { d.AddField("Reconciler", "ran") }), }, + }, { + Name: "reconciler mutated finalizers", + Key: testKey, + GivenObjects: []client.Object{ + resource, + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + Finalize: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + } + }, + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", + `Updated finalizers`), + }, + ExpectUpdates: []client.Object{ + resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers("testing.reconciler.runtime/1-finalizer") + }).SpecDie(func(d *dies.TestResourceSpecDie) { + d.AddField("Defaulter", "ran") + }), + }, }, { Name: "sub reconciler erred", Key: testKey, @@ -304,197 +314,49 @@ func TestParentReconciler(t *testing.T) { }, }, }, { - Name: "has Finalize", - Key: testKey, - GivenObjects: []client.Object{ - resource, - }, - ExpectUpdates: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.Finalizers(finalizer) - }).SpecDie(func(d *dies.TestResourceSpecDie) { - d.AddField("Defaulter", "ran") - }), - }, - ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), - }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.Sequence{} - }, - "Finalize": func(ctx context.Context, parent *resources.TestResource) error { - return nil - }, - }, - }, { - Name: "has finalizer", + Name: "finalizers update failed", Key: testKey, GivenObjects: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.Finalizers(finalizer) - }), - }, - ExpectUpdates: []client.Object{ resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Finalizers() - }).SpecDie(func(d *dies.TestResourceSpecDie) { - d.AddField("Defaulter", "ran") }), }, - ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), - }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.Sequence{} - }, - }, - }, { - Name: "has Finalize and has finalizer", - Key: testKey, - GivenObjects: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.Finalizers(finalizer) - }), - }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.Sequence{} - }, - "Finalize": func(ctx context.Context, parent *resources.TestResource) error { - return nil - }, - }, - }, { - Name: "has finalize, has finalizer, and is deleted", - Key: testKey, - GivenObjects: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.DeletionTimestamp(&deletedAt) - d.Finalizers(finalizer) - }), + WithReactors: []rtesting.ReactionFunc{ + rtesting.InduceFailure("update", "TestResource"), }, ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), + rtesting.NewEvent(resource, scheme, corev1.EventTypeWarning, "UpdateFailed", + `Failed to update finalizers: inducing failure for update TestResource`), }, ExpectUpdates: []client.Object{ resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.DeletionTimestamp(&deletedAt) - d.Finalizers() + d.Finalizers("testing.reconciler.runtime/1-finalizer") }).SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("Defaulter", "ran") }), }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.Sequence{} - }, - "Finalize": func(ctx context.Context, parent *resources.TestResource) error { - return nil - }, - }, - }, { - Name: "Finalize erred", - Key: testKey, - GivenObjects: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.DeletionTimestamp(&deletedAt) - d.Finalizers(finalizer) - }), - }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.Sequence{} - }, - "Finalize": func(ctx context.Context, parent *resources.TestResource) error { - return fmt.Errorf("Finalize error") - }, - }, - ShouldErr: true, - }, { - Name: "has Finalize and status", - Key: testKey, - GivenObjects: []client.Object{ - resource, - }, - ExpectUpdates: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.Finalizers(finalizer) - }).SpecDie(func(d *dies.TestResourceSpecDie) { - d.AddField("Defaulter", "ran") - }).StatusDie(func(d *dies.TestResourceStatusDie) { - d.AddField("Reconciler", "ran") - }), - }, - ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "StatusUpdated", - `Updated status`), - rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Updated", `Updated finalizers`), - }, - ExpectStatusUpdates: []client.Object{ - resource.StatusDie(func(d *dies.TestResourceStatusDie) { - d.AddField("Reconciler", "ran") - }), - }, Metadata: map[string]interface{}{ "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { return &reconcilers.SyncReconciler{ Config: c, Sync: func(ctx context.Context, parent *resources.TestResource) error { - if parent.Status.Fields == nil { - parent.Status.Fields = map[string]string{} - } - parent.Status.Fields["Reconciler"] = "ran" + return nil + }, + Finalize: func(ctx context.Context, parent *resources.TestResource) error { return nil }, } }, - "Finalize": func(ctx context.Context, parent *resources.TestResource) error { - return nil - }, - }, - }, { - Name: "finalizers update failed", - Key: testKey, - GivenObjects: []client.Object{ - resource, - }, - WithReactors: []rtesting.ReactionFunc{ - rtesting.InduceFailure("update", "TestResource"), - }, - ExpectUpdates: []client.Object{ - resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.Finalizers(finalizer) - }).SpecDie(func(d *dies.TestResourceSpecDie) { - d.AddField("Defaulter", "ran") - }), - }, - ExpectEvents: []rtesting.Event{ - rtesting.NewEvent(resource, scheme, corev1.EventTypeWarning, "UpdateFailed", `Failed to update finalizers: inducing failure for update TestResource`), - }, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.Sequence{} - }, - "Finalize": func(ctx context.Context, parent *resources.TestResource) error { - return nil - }, }, ShouldErr: true, }} rts.Test(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - reconciler := &reconcilers.ParentReconciler{ + return &reconcilers.ParentReconciler{ Type: &resources.TestResource{}, Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), Config: c, } - if _, ok := rtc.Metadata["Finalize"]; ok { - reconciler.Finalize = rtc.Metadata["Finalize"].(func(context.Context, *resources.TestResource) error) - } - - return reconciler }) } @@ -516,6 +378,7 @@ func TestSyncReconciler(t *testing.T) { diemetav1.ConditionBlank.Type(apis.ConditionReady).Status(metav1.ConditionUnknown).Reason("Initializing"), ) }) + deletedAt := metav1.NewTime(time.UnixMilli(2000)) rts := rtesting.SubReconcilerTestSuite{{ Name: "sync success", @@ -570,6 +433,104 @@ func TestSyncReconciler(t *testing.T) { }, }, ShouldPanic: true, + }, { + Name: "has Finalize", + Parent: resource, + ExpectParent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers("/1-finalizer") + }), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + Finalize: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + } + }, + }, + }, { + Name: "has finalizer", + Parent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers("/1-finalizer") + }), + ExpectParent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers() + }), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + } + }, + }, + }, { + Name: "has Finalize and has finalizer", + Parent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers("/1-finalizer") + }), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + Finalize: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + } + }, + }, + }, { + Name: "has finalize, has finalizer, and is deleted", + Parent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletedAt) + d.Finalizers("/1-finalizer") + }), + ExpectParent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletedAt) + d.Finalizers() + }), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + Finalize: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + } + }, + }, + }, { + Name: "Finalize erred", + Parent: resource.MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletedAt) + d.Finalizers("/1-finalizer") + }), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return &reconcilers.SyncReconciler{ + Config: c, + Sync: func(ctx context.Context, parent *resources.TestResource) error { + return nil + }, + Finalize: func(ctx context.Context, parent *resources.TestResource) error { + return fmt.Errorf("Finalize error") + }, + } + }, + }, + ShouldErr: true, }} rts.Test(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { @@ -603,6 +564,8 @@ func TestChildReconciler(t *testing.T) { ) }) + deletionTimestamp := metav1.NewTime(time.UnixMilli(2000)) + configMapCreate := diecorev1.ConfigMapBlank. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Namespace(testNamespace) @@ -669,6 +632,20 @@ func TestChildReconciler(t *testing.T) { return defaultChildReconciler(c) }, }, + }, { + Name: "ignore deleted", + Parent: resourceReady. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.DeletionTimestamp(&deletionTimestamp) + }), + GivenObjects: []client.Object{ + configMapGiven, + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + return defaultChildReconciler(c) + }, + }, }, { Name: "child is in sync", Parent: resourceReady. @@ -1604,3 +1581,17 @@ func TestCastParent(t *testing.T) { return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c) }) } + +func TestUniqueFinalizer(t *testing.T) { + ctx := reconcilers.WithStash(context.Background()) + + finalizer1 := reconcilers.UniqueFinalizer(ctx, "group.com") + if diff := cmp.Diff("group.com/1-finalizer", finalizer1); diff != "" { + t.Errorf("Unexpected delete (-expected, +actual): %s", diff) + } + + finalizer2 := reconcilers.UniqueFinalizer(ctx, "group.com") + if diff := cmp.Diff("group.com/2-finalizer", finalizer2); diff != "" { + t.Errorf("Unexpected delete (-expected, +actual): %s", diff) + } +} diff --git a/reconcilers/reconcilers_validate_test.go b/reconcilers/reconcilers_validate_test.go index 00255cf..b1f0838 100644 --- a/reconcilers/reconcilers_validate_test.go +++ b/reconcilers/reconcilers_validate_test.go @@ -14,100 +14,38 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func TestParentReconciler_validate(t *testing.T) { +func TestSyncReconciler_validate(t *testing.T) { tests := []struct { name string parent client.Object - reconciler *ParentReconciler + reconciler *SyncReconciler shouldErr string }{ { name: "empty", parent: &corev1.ConfigMap{}, - reconciler: &ParentReconciler{}, + reconciler: &SyncReconciler{}, + shouldErr: "SyncReconciler must implement Sync", }, { name: "valid", parent: &corev1.ConfigMap{}, - reconciler: &ParentReconciler{ - Finalize: func(ctx context.Context, parent *corev1.ConfigMap) error { - return nil - }, - }, - }, - { - name: "Finalize num in", - parent: &corev1.ConfigMap{}, - reconciler: &ParentReconciler{ - Finalize: func() error { - return nil - }, - }, - shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func() error", - }, - { - name: "Finalize in 1", - parent: &corev1.ConfigMap{}, - reconciler: &ParentReconciler{ - Finalize: func(ctx context.Context, parent *corev1.Secret) error { + reconciler: &SyncReconciler{ + Sync: func(ctx context.Context, parent *corev1.ConfigMap) error { return nil }, }, - shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.Secret) error", }, { - name: "Finalize num out", - parent: &corev1.ConfigMap{}, - reconciler: &ParentReconciler{ - Finalize: func(ctx context.Context, parent *corev1.ConfigMap) (ctrl.Result, error) { - return ctrl.Result{}, nil - }, - }, - shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.ConfigMap) (reconcile.Result, error)", - }, - { - name: "Finalize out 1", - parent: &corev1.ConfigMap{}, - reconciler: &ParentReconciler{ - Finalize: func(ctx context.Context, parent *corev1.ConfigMap) *ctrl.Result { - return nil - }, - }, - shouldErr: "ParentReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.ConfigMap) *reconcile.Result", - }, - } - - for _, c := range tests { - t.Run(c.name, func(t *testing.T) { - ctx := StashCastParentType(context.TODO(), c.parent) - err := c.reconciler.validate(ctx) - if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) { - t.Errorf("validate() error = %q, shouldErr %q", err, c.shouldErr) - } - }) - } -} - -func TestSyncReconciler_validate(t *testing.T) { - tests := []struct { - name string - parent client.Object - reconciler *SyncReconciler - shouldErr string - }{ - { - name: "empty", - parent: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{}, - shouldErr: "SyncReconciler must implement Sync", - }, - { - name: "valid", + name: "valid with Finalize", parent: &corev1.ConfigMap{}, reconciler: &SyncReconciler{ Sync: func(ctx context.Context, parent *corev1.ConfigMap) error { return nil }, + Finalize: func(ctx context.Context, parent *corev1.ConfigMap) error { + return nil + }, }, }, { @@ -168,6 +106,58 @@ func TestSyncReconciler_validate(t *testing.T) { }, shouldErr: "SyncReconciler must implement Sync: func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap) (reconcile.Result, string)", }, + { + name: "Finalize num in", + parent: &corev1.ConfigMap{}, + reconciler: &SyncReconciler{ + Sync: func(ctx context.Context, parent *corev1.ConfigMap) error { + return nil + }, + Finalize: func() error { + return nil + }, + }, + shouldErr: "SyncReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func() error", + }, + { + name: "Finalize in 1", + parent: &corev1.ConfigMap{}, + reconciler: &SyncReconciler{ + Sync: func(ctx context.Context, parent *corev1.ConfigMap) error { + return nil + }, + Finalize: func(ctx context.Context, parent *corev1.Secret) error { + return nil + }, + }, + shouldErr: "SyncReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.Secret) error", + }, + { + name: "Finalize num out", + parent: &corev1.ConfigMap{}, + reconciler: &SyncReconciler{ + Sync: func(ctx context.Context, parent *corev1.ConfigMap) error { + return nil + }, + Finalize: func(ctx context.Context, parent *corev1.ConfigMap) (ctrl.Result, error) { + return ctrl.Result{}, nil + }, + }, + shouldErr: "SyncReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.ConfigMap) (reconcile.Result, error)", + }, + { + name: "Finalize out 1", + parent: &corev1.ConfigMap{}, + reconciler: &SyncReconciler{ + Sync: func(ctx context.Context, parent *corev1.ConfigMap) error { + return nil + }, + Finalize: func(ctx context.Context, parent *corev1.ConfigMap) *ctrl.Result { + return nil + }, + }, + shouldErr: "SyncReconciler Finalize must have correct signature: func(context.Context, *v1.ConfigMap) error, found: func(context.Context, *v1.ConfigMap) *reconcile.Result", + }, } for _, c := range tests {