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
Optional finalize for parent reconciler #204
Conversation
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
- Used to clean up unowned resources on a delete reconcile.
bfacff2
to
b8fd9d1
Compare
There was a problem hiding this 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.
reconcilers/reconcilers.go
Outdated
// Expected function signature: | ||
// func(ctx context.Context, parent client.Object) error | ||
// +optional | ||
Finalize interface{} |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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!
|
||
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 { |
There was a problem hiding this comment.
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.
reconcilers/reconcilers.go
Outdated
// 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) |
There was a problem hiding this comment.
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".
finalizer := fmt.Sprintf("%s/reconciler-runtime-finalize", parent.GetObjectKind().GroupVersionKind().Group) | |
finalizer := fmt.Sprintf("%s/reconciler-runtime-finalizer", parent.GetObjectKind().GroupVersionKind().Group) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Co-authored-by: Scott Andrews <scott@andrews.me>
7f06178
to
47f6d8b
Compare
- ParentReconciler allows delete reconciles - ChildReconciler ignores them - Each syncReconciler adds a unique finalizer (supplied via stash)
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:
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. |
@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. |
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:This method would be called on a delete reconcile and if successful, finalizer is removed.
Allow delete reconciles for
ParentReconciler
if theFinalize
method is present.Add the a deterministic finalizer on non-delete reconciles of
ParentReconciler
if theFinalize
method is present.