Skip to content

Commit a00e2b3

Browse files
feat(ctrl): encapsulates finalizer logic in reusable pkg (openshift-service-mesh#164)
We are going to leverage finalizers to ensure we properly release any resources owned by our controllers. That includes resources we cannot garbage collect using `ownerRef` but also things like gRPC connections we are going to establish between meshes. Providing consistent approach to deal with this scenario helps us write cleaner and maintainable code. This change brings `finalizer` package to address this. It has `finalizer.Handler` struct that: - adds finalizer to a given object if it does not exists and then persist it - performs cleanup defined as `CleanupFn` if the object is marked for deletion Example usage in the controller reconcile loop: ```golang finalizerHandler := finalizer.NewHandler(r.Client, "federation.openshift-service-mesh.io/mesh-federation") if finalized, errFinalize := finalizerHandler.Finalize(ctx, meshFederation, func() error { // finalizer logic goes here return nil }); finalized { return ctrl.Result{}, errFinalize } if finalizerAlreadyExists, errAdd := finalizerHandler.Add(ctx, meshFederation); !finalizerAlreadyExists { return ctrl.Result{}, errAdd } ``` It seems to make sense to trigger another reconcile immediately after adding finalizer, to ensure that the object the controller reconciles gets it as soon as possible. ### Additional changes Together with `finalizer` a set of common `retry` functions has been added, so that `finalizer.Handler` can retry updating the object when the conflict occurs. Signed-off-by: bartoszmajsak <[email protected]> --------- Signed-off-by: bartoszmajsak <[email protected]> Co-authored-by: Eoin Fennessy <[email protected]>
1 parent 18390b0 commit a00e2b3

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright Red Hat, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the License);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an AS IS BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package finalizer
16+
17+
import (
18+
"context"
19+
20+
"sigs.k8s.io/controller-runtime/pkg/client"
21+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
22+
23+
"github.com/openshift-service-mesh/federation/internal/controller"
24+
)
25+
26+
// CleanupFn is a closure which can be used in the reconciler to define finalizer logic just before
27+
// the object is removed from kube-apiserver.
28+
type CleanupFn func() error
29+
30+
// Handler encapsulates finalizer handling. It can:
31+
// - add finalizer to a given object and persist it
32+
// - perform cleanup defined as CleanupFn if the object is marked for deletion
33+
//
34+
// Example usage in the controller reconcile loop:
35+
//
36+
// finalizerHandler := finalizer.NewHandler(r.Client, "federation.openshift-service-mesh.io/mesh-federation")
37+
// if finalized, errFinalize := finalizerHandler.Finalize(ctx, meshFederation, func() error {
38+
// // finalizer logic
39+
// return nil
40+
// }); finalized {
41+
// return ctrl.Result{}, errFinalize
42+
// }
43+
//
44+
// if finalizerAlreadyExists, errAdd := finalizerHandler.Add(ctx, meshFederation); !finalizerAlreadyExists {
45+
// return ctrl.Result{}, errAdd
46+
// }
47+
type Handler struct {
48+
cl client.Client
49+
finalizerName string
50+
}
51+
52+
func NewHandler(cl client.Client, finalizerName string) *Handler {
53+
return &Handler{
54+
cl: cl,
55+
finalizerName: finalizerName,
56+
}
57+
}
58+
59+
// Add adds the defined finalizer to the object and then persists it if the finalizer was not already present.
60+
// Returns true if the finalizer was already present.
61+
// Returns an error if updating the object failed.
62+
func (f *Handler) Add(ctx context.Context, obj client.Object) (bool, error) {
63+
if finalizersUpdated := controllerutil.AddFinalizer(obj, f.finalizerName); !finalizersUpdated {
64+
return true, nil // Finalizer already exists, no need to add it
65+
}
66+
67+
_, errRetry := controller.RetryUpdate(ctx, f.cl, obj, func(saved client.Object) {
68+
controllerutil.AddFinalizer(saved, f.finalizerName) // in case of conflict retry adding finalizer on the obj fetched from cluster
69+
})
70+
71+
return false, errRetry
72+
}
73+
74+
// Finalize executes cleanup logic defined in cleanupFn only if the object is marked for deletion.
75+
// Returns true if finalizer logic was attempted.
76+
// Returns an error if the cleanup function was unsuccessful or the removal of the finalizer failed.
77+
func (f *Handler) Finalize(ctx context.Context, obj client.Object, cleanupFn CleanupFn) (bool, error) {
78+
if finalizeNeeded := !obj.GetDeletionTimestamp().IsZero() && controllerutil.ContainsFinalizer(obj, f.finalizerName); !finalizeNeeded {
79+
return false, nil
80+
}
81+
82+
if err := cleanupFn(); err != nil {
83+
return true, err
84+
}
85+
86+
_, errRetry := controller.RetryUpdate(ctx, f.cl, obj, func(saved client.Object) {
87+
controllerutil.RemoveFinalizer(saved, f.finalizerName) // in case of conflict retry removing finalizer on the obj fetched from cluster
88+
})
89+
90+
return true, errRetry
91+
}

internal/controller/retry.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright Red Hat, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the License);
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an AS IS BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controller
16+
17+
import (
18+
"context"
19+
"errors"
20+
21+
"k8s.io/client-go/util/retry"
22+
"sigs.k8s.io/controller-runtime/pkg/client"
23+
)
24+
25+
// MutateFn is a function that allows defining custom
26+
// logic for updating resource object.
27+
type MutateFn[T client.Object] func(saved T)
28+
29+
// ClientCallFn defines what client.Client operation on a given object should be performed.
30+
type ClientCallFn[T client.Object] func(ctx context.Context, cli client.Client, obj T) error
31+
32+
// RetryUpdate attempts to update a specified Kubernetes resource and retries on conflict.
33+
func RetryUpdate[T client.Object](ctx context.Context, cli client.Client, original T, mutate MutateFn[T], opts ...client.UpdateOption) (T, error) {
34+
updateObjFn := func(ctx context.Context, cli client.Client, obj T) error {
35+
return cli.Update(ctx, obj, opts...)
36+
}
37+
38+
return retryCall[T](ctx, cli, original, mutate, updateObjFn)
39+
}
40+
41+
// RetryStatusUpdate attempts to update status subresource of a specified Kubernetes resource and retries on conflict.
42+
func RetryStatusUpdate[T client.Object](ctx context.Context, cli client.Client, original T, mutate MutateFn[T], opts ...client.SubResourceUpdateOption) (T, error) {
43+
updateStatusFn := func(ctx context.Context, cli client.Client, obj T) error {
44+
return cli.Status().Update(ctx, obj, opts...)
45+
}
46+
47+
return retryCall[T](ctx, cli, original, mutate, updateStatusFn)
48+
}
49+
50+
func retryCall[T client.Object](
51+
ctx context.Context,
52+
cli client.Client,
53+
original T,
54+
mutate MutateFn[T],
55+
updateFn ClientCallFn[T],
56+
) (T, error) {
57+
58+
current, ok := original.DeepCopyObject().(T)
59+
if !ok {
60+
var zero T
61+
return zero, errors.New("failed to deep copy object")
62+
}
63+
64+
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
65+
if err := cli.Get(ctx, client.ObjectKeyFromObject(original), current); err != nil {
66+
return err
67+
}
68+
mutate(current)
69+
70+
// Return the mutate error directly so the RetryOnConflict logic can identify conflicts
71+
return updateFn(ctx, cli, current)
72+
})
73+
74+
return current, err
75+
}

0 commit comments

Comments
 (0)