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

Optional finalize for parent reconciler #204

Conversation

gmrodgers
Copy link
Contributor

@gmrodgers gmrodgers commented Apr 6, 2022

Currently, the ParentReconciler does not reconcile on a delete request. This leads to some reconcilers not being implementable in reconciler-runtime if they want to do cleanup on a delete (outside of owner reference garbage collection).

The typical way to do this in Kubernetes is to have the reconciler add a finalizer to the resource on first reconcile which blocks deletion, and when it reconciles on deletion, it cleans up and removes said finalizer (see: Finalizers).

This PR aims to create an abstraction over the finalizers, seeing them as an implementation detail. It proposes the following:

  • An optional Finalize method on the Parent reconciler with signature:

    func(ctx context.Context, parent client.Object) error

    This method would be called on a delete reconcile and if successful, finalizer is removed.

  • Allow delete reconciles for ParentReconciler if the Finalize method is present.

  • Add the a deterministic finalizer on non-delete reconciles of ParentReconciler if the Finalize method is present.

@codecov
Copy link

codecov bot commented Apr 6, 2022

Codecov Report

Merging #204 (c8e660d) into main (0248645) will increase coverage by 1.66%.
The diff coverage is 95.65%.

❗ Current head c8e660d differs from pull request most recent head a2b3906. Consider uploading reports for the commit a2b3906 to get more accurate results

@@            Coverage Diff             @@
##             main     #204      +/-   ##
==========================================
+ Coverage   64.71%   66.37%   +1.66%     
==========================================
  Files           8        8              
  Lines         768      812      +44     
==========================================
+ Hits          497      539      +42     
- Misses        256      257       +1     
- Partials       15       16       +1     
Impacted Files Coverage Δ
reconcilers/reconcilers.go 85.91% <95.65%> (+0.91%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0248645...a2b3906. Read the comment docs.

- Used to clean up unowned resources on a delete reconcile.
@gmrodgers gmrodgers force-pushed the optional-finalize-for-parent-reconciler branch from bfacff2 to b8fd9d1 Compare April 6, 2022 21:31
Copy link
Contributor

@scothis scothis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for kicking off this work. Depending on your level of interest there are a couple paths we can explore based on inline comments/questions.

While I think this approach will work for the vast majority of cases, I want to consider what it would mean to bake finalizers a bit deeper into the composition model.

// Expected function signature:
// func(ctx context.Context, parent client.Object) error
// +optional
Finalize interface{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if Finalize should itself be a SubReconciler? That would give us a natural way to compose multiple finalizers. We'd consider the finalizer clearable when it returns an empty result and no error.

Another option would be to add a Finalize method to the SubReconciler interface (or create an extended interface to not break existing types).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this too but wonder how some subreconcilers would behave in the case of deletion. SyncReconcilers are a natural fit (even share the same signature as what I implemented). ChildReconcilers seem to be somewhere we do not want to have on deletion. If we were to go down that route, would we prevent ChildReconcilers some how or just rely on user restraint?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd want finalizer support for each built-in sub reconciler. A ChildReconciler that creates a resource in a different namespace should still be able to clean up that resource upon deletion of the parent. The sub reconciler, especially when composed, will have more context about the state that needs to be cleaned up than the parent reconciler.

Copy link
Contributor Author

@gmrodgers gmrodgers Apr 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, so to check I understand, given SyncReconciler and ChildReconciler as examples:

  • The ChildReconciler would create a finalizer if a) itself is a namespaced scope resource, and b) it's child is either clusterscoped or in a different namespace. On a deletion, it would then have remove the parent finalizer, and the child resource using an explicit Delete.
  • The SyncReconciler would need to have its functionality exposed via a Finalize like method which if present, a finalizer is added to the parent. On a deletion, it would then remove the finalizer after running Finalize.

And by necessity, the ParentReconciler will now allow deletion requests through to subreconcilers (unless it were to check all whether all subreconcilers need to create finalizers).

Copy link
Contributor Author

@gmrodgers gmrodgers Apr 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ive pushed a version that does part of the above:

  • SyncReconciler adds a finalizer and has the new method
  • For now, ChildReconciler just ignores it and will assume same namespace tidy up via ownerreferences

The tricky bit I found was how to allow for a unique finalizer for each Sync (or any other reconciler that wanted one). I came up with the following options:

  • Hash the contents of the Finalize function
    • I don't think this works the Finalize functions shared across SubReconciler
  • The name of Subreconciler via reflection
    • Problem since you could also have same named reconcilers but different packages, or inlined SubReconcilers
  • Have user pass in their own custom finalizers
    • feels like breaking the abstraction by allowing selection of finalizers perhaps?
    • Easiest solution that would probably cover most usecases
  • Have unique finalizer generated per reconciler via incrementing a count in the stash (I tried implementing this)
    • Had to create a UniqueFinalizer() func to handle this and make it public so other Subreconcilers could use it.

Open to thoughts on the options or suggestions of others!

reconcilers/reconcilers.go Outdated Show resolved Hide resolved
reconcilers/reconcilers.go Outdated Show resolved Hide resolved

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A patch request that only updates the finalizers would be better, as to avoid any accidental mutations to other parts of the resource. Early patch support is in #202

This is fine as a stopgap, until that merges.

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think there is any need to allow a fully custom finalizer name?

There's a corner case for "v1" resources that are part of the "core" api group, but are represented by an empty group string in the gvk. To avoid a finalizer that starts with a "/", we should expand "" to "core".

Suggested change
finalizer := fmt.Sprintf("%s/reconciler-runtime-finalize", parent.GetObjectKind().GroupVersionKind().Group)
finalizer := fmt.Sprintf("%s/reconciler-runtime-finalizer", parent.GetObjectKind().GroupVersionKind().Group)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, as in allow the writer of the ParentReconciler to specify a custom finalizer? I guess they would want this if a) they need more than one finalizer or b) they need a finalizer with a specific name. I have not seen a) or b) in the wild and therefore am unsure if it is a usecase or if it is best practice or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A resource can define an arbitrary set of finalizers, k8s will only finish the deletion once all of the finalizers have been removed.

@gmrodgers gmrodgers force-pushed the optional-finalize-for-parent-reconciler branch from 7f06178 to 47f6d8b Compare April 8, 2022 14:38
- ParentReconciler allows delete reconciles
- ChildReconciler ignores them
- Each syncReconciler adds a unique finalizer (supplied via stash)
@scothis
Copy link
Contributor

scothis commented Apr 17, 2022

I started to play around with pushing the bulk of finalizer support into the edges of the reconciler tree, allowing each child reconciler to manage its own finalizer on the parent. There's certainly a trade off as a poorly crafted reconciler could causes a child to be orphaned or a resource to be stuck terminating.

The approach you took in this PR is still useful, but it may may more sense as a higher order sub reconciler. I'm imaging something with a similar shape to the CastParent reconciler:

  • when the parent resource is not terminating and the finalizer is not set, add it
  • call the nested sub reconciler
  • when the parent resource is terminating and the nested sub reconciler did not return an error, clear the finalizer

This pattern could be used to create arbitrary finalization boundaries anywhere in the sub reconciler hierarchy. There are a couple helper utilities in my PR that make adding/clearing a finalizer on the parent resource much easier.

@scothis
Copy link
Contributor

scothis commented May 6, 2022

@gmrodgers thank you so much for opening this PR and kick-starting the conversation around finalizers. I'm going to close this as it largely conflicts with #207

There are a couple good ideas in this PR that are not present in #207, specifically being able to define adding/removing a finalizer around a reconciliation. Where #207 adds finalizer support for the leaves, we could also support finalizers at the trunk of the reconciler tree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants