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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
log.Error(updateErr, "unable to update", typeName(r.Type), parent) | ||||||
r.Recorder.Eventf(parent, corev1.EventTypeWarning, "UpdateFailed", | ||||||
"Failed to update: %v", updateErr) | ||||||
gmrodgers marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return ctrl.Result{}, updateErr | ||||||
} | ||||||
r.Recorder.Eventf(parent, corev1.EventTypeNormal, "Updated", "Updated") | ||||||
gmrodgers marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
// 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 commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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. |
||||||
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() | ||||||
} | ||||||
|
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:
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:
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:
Open to thoughts on the options or suggestions of others!