Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional clearing of finalizers #525

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading