Skip to content

Commit

Permalink
Merge pull request #525 from scothis/clear-finalizer-conditional
Browse files Browse the repository at this point in the history
  • Loading branch information
scothis committed May 22, 2024
2 parents f3cbf1e + 7d81d03 commit c91817b
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 7 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,9 @@ func SwapRESTConfig(rc *rest.Config) *reconcilers.SubReconciler[*resources.MyRes

#### WithFinalizer

[`WithFinalizer`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#WithFinalizer) allows external state to be allocated and then cleaned up once the resource is deleted. When the resource is not terminating, the finalizer is set on the reconciled resource before the nested reconciler is called. When the resource is terminating, the finalizer is cleared only after the nested reconciler returns without an error.
[`WithFinalizer`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#WithFinalizer) allows external state to be allocated and then cleaned up once the resource is deleted. When the resource is not terminating, the finalizer is set on the reconciled resource before the nested reconciler is called. When the resource is terminating, the finalizer is cleared only after the nested reconciler returns without an error and `ReadyToClearFinalizer` returns `true`.

`ReadyToClearFinalizer` can be used to define custom rules for clearing the finalizer. For example, deletion of a resource can be blocked until all child resources are fully deleted replicating the behavior of an owner reference in parent-child relationships that are not supported by owner references. Client lookups and advanced logic should be avoided as errors cannot be returned. Computed values can be retrieved that were stashed from a previous reconciler, like a [`SyncReconciler#Finalize`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#SyncReconciler.Finalize) hook.

The [Finalizers](#finalizers) utilities are used to manage the finalizer on the reconciled resource.

Expand Down
35 changes: 30 additions & 5 deletions reconcilers/finalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"sync"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
Expand All @@ -36,7 +37,8 @@ var (
// WithFinalizer ensures the resource being reconciled has the desired finalizer set so that state
// can be cleaned up upon the resource being deleted. The finalizer is added to the resource, if not
// already set, before calling the nested reconciler. When the resource is terminating, the
// finalizer is cleared after returning from the nested reconciler without error.
// finalizer is cleared after returning from the nested reconciler without error and
// ReadyToClearFinalizer returns true.
type WithFinalizer[Type client.Object] struct {
// Name used to identify this reconciler. Defaults to `WithFinalizer`. Ideally unique, but
// not required to be so.
Expand All @@ -52,16 +54,37 @@ type WithFinalizer[Type client.Object] struct {
// is fully deleted. This commonly include state allocated outside of the current cluster.
Finalizer string

// ReadyToClearFinalizer must return true before the finalizer is cleared from the resource.
// Only called when the resource is terminating.
//
// Defaults to always return true.
//
// +optional
ReadyToClearFinalizer func(ctx context.Context, resource Type) bool

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

initOnce sync.Once
}

func (r *WithFinalizer[T]) init() {
r.initOnce.Do(func() {
if r.Name == "" {
r.Name = "WithFinalizer"
}
if r.ReadyToClearFinalizer == nil {
r.ReadyToClearFinalizer = func(ctx context.Context, resource T) bool {
return true
}
}
})
}

func (r *WithFinalizer[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error {
if r.Name == "" {
r.Name = "WithFinalizer"
}
r.init()

log := logr.FromContextOrDiscard(ctx).
WithName(r.Name)
Expand All @@ -88,6 +111,8 @@ func (r *WithFinalizer[T]) validate(ctx context.Context) error {
}

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

log := logr.FromContextOrDiscard(ctx).
WithName(r.Name)
ctx = logr.NewContext(ctx, log)
Expand All @@ -101,7 +126,7 @@ func (r *WithFinalizer[T]) Reconcile(ctx context.Context, resource T) (Result, e
if err != nil {
return result, err
}
if resource.GetDeletionTimestamp() != nil {
if resource.GetDeletionTimestamp() != nil && r.ReadyToClearFinalizer(ctx, resource) {
if err := ClearFinalizer(ctx, resource, r.Finalizer); err != nil {
return Result{}, err
}
Expand Down
32 changes: 31 additions & 1 deletion reconcilers/finalizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,31 @@ func TestWithFinalizer(t *testing.T) {
},
},
},
"keep finalizer until ready": {
Resource: resource.
MetadataDie(func(d *diemetav1.ObjectMetaDie) {
d.DeletionTimestamp(now)
d.Finalizers(testFinalizer)
}).
DieReleasePtr(),
ExpectResource: resource.
MetadataDie(func(d *diemetav1.ObjectMetaDie) {
d.DeletionTimestamp(now)
d.Finalizers(testFinalizer)
}).
DieReleasePtr(),
ExpectEvents: []rtesting.Event{
rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Finalize", ""),
rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "ReadyToClearFinalizer", "not ready"),
},
Metadata: map[string]interface{}{
"ReadyToClearFinalizer": func(ctx context.Context, resource *resources.TestResource) bool {
c := reconcilers.RetrieveConfigOrDie(ctx)
c.Recorder.Event(resource, corev1.EventTypeNormal, "ReadyToClearFinalizer", "not ready")
return false
},
},
},
"clear finalizer": {
Resource: resource.
MetadataDie(func(d *diemetav1.ObjectMetaDie) {
Expand Down Expand Up @@ -186,9 +211,14 @@ func TestWithFinalizer(t *testing.T) {
if err, ok := rtc.Metadata["FinalizerError"]; ok {
finalizeErr = err.(error)
}
var readyToClearFinalizer func(context.Context, *resources.TestResource) bool
if ready, ok := rtc.Metadata["ReadyToClearFinalizer"]; ok {
readyToClearFinalizer = ready.(func(context.Context, *resources.TestResource) bool)
}

return &reconcilers.WithFinalizer[*resources.TestResource]{
Finalizer: testFinalizer,
Finalizer: testFinalizer,
ReadyToClearFinalizer: readyToClearFinalizer,
Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{
Sync: func(ctx context.Context, resource *resources.TestResource) error {
c.Recorder.Event(resource, corev1.EventTypeNormal, "Sync", "")
Expand Down

0 comments on commit c91817b

Please sign in to comment.