Skip to content

Commit f3f33dd

Browse files
authored
use json merge patch to update finalizers (#402)
1 parent 6335db2 commit f3f33dd

5 files changed

Lines changed: 54 additions & 5 deletions

File tree

internal/helm/util.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"strings"
1111
)
1212

13+
// TODO: consolidate all the util files into an internal reuse package
14+
1315
/*
1416
func getString(data map[string]any, key string) (string, bool, bool) {
1517
if v, ok := data[key]; ok {

internal/util/finalizer.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package util
7+
8+
import (
9+
"context"
10+
"encoding/json"
11+
12+
apitypes "k8s.io/apimachinery/pkg/types"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
// This is a workaround. Ideally Kubernetes would have a finalizers subresource, and controller-runtime
17+
// would offer the according subresource client.
18+
func UpdateFinalizers(ctx context.Context, clnt client.Client, obj client.Object, fieldOwner string) error {
19+
// note: it's crucial to use string keyed maps to construct the patch; using metav1.ObjectMeta would lead to
20+
// finalizers to be removed entirely from the patch when empty (because of omitempty usage there);
21+
// but we need to preserve empty or null finalizers in the patch, in order to make JSON merge patch apply it correctly
22+
finalizerPatch := map[string]any{
23+
"metadata": map[string]any{
24+
"resourceVersion": obj.GetResourceVersion(),
25+
"finalizers": obj.GetFinalizers(),
26+
},
27+
}
28+
// note: this must() is ok because marshalling finalizers should always work
29+
return clnt.Patch(ctx, obj, client.RawPatch(apitypes.MergePatchType, must(json.Marshal(finalizerPatch))), client.FieldOwner(fieldOwner))
30+
}

internal/util/util.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package util
7+
8+
// TODO: consolidate all the util files into an internal reuse package
9+
10+
func must[T any](x T, err error) T {
11+
if err != nil {
12+
panic(err)
13+
}
14+
return x
15+
}

pkg/component/reconciler.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545
"github.com/sap/component-operator-runtime/internal/clientfactory"
4646
"github.com/sap/component-operator-runtime/internal/events"
4747
"github.com/sap/component-operator-runtime/internal/metrics"
48+
"github.com/sap/component-operator-runtime/internal/util"
4849
"github.com/sap/component-operator-runtime/pkg/cluster"
4950
"github.com/sap/component-operator-runtime/pkg/manifests"
5051
"github.com/sap/component-operator-runtime/pkg/reconciler"
@@ -514,7 +515,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
514515
// create/update case
515516
// TODO: optionally (to be completely consistent) set finalizer through a mutating webhook
516517
if added := controllerutil.AddFinalizer(component, *r.options.Finalizer); added {
517-
if err := r.client.Update(ctx, component, client.FieldOwner(*r.options.FieldOwner)); err != nil {
518+
if err := util.UpdateFinalizers(ctx, r.client, component, *r.options.FieldOwner); err != nil {
518519
return ctrl.Result{}, legacyerrors.Wrap(err, "error adding finalizer")
519520
}
520521
// trigger another round trip
@@ -610,7 +611,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
610611
// all dependent resources are already gone, so that's it
611612
log.V(1).Info("all dependent resources are successfully deleted; removing finalizer")
612613
if removed := controllerutil.RemoveFinalizer(component, *r.options.Finalizer); removed {
613-
if err := r.client.Update(ctx, component, client.FieldOwner(*r.options.FieldOwner)); err != nil {
614+
if err := util.UpdateFinalizers(ctx, r.client, component, *r.options.FieldOwner); err != nil {
614615
return ctrl.Result{}, legacyerrors.Wrap(err, "error removing finalizer")
615616
}
616617
}

pkg/reconciler/reconciler.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3535
"sigs.k8s.io/controller-runtime/pkg/log"
3636

37+
"github.com/sap/component-operator-runtime/internal/util"
3738
"github.com/sap/component-operator-runtime/pkg/cluster"
3839
"github.com/sap/component-operator-runtime/pkg/status"
3940
"github.com/sap/component-operator-runtime/pkg/types"
@@ -1318,7 +1319,7 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis
13181319
}
13191320
if ok := controllerutil.RemoveFinalizer(crd, r.finalizer); ok {
13201321
// note: 409 error is very likely here (because of concurrent updates happening through the api server); this is why we retry once
1321-
if err := r.client.Update(ctx, crd, client.FieldOwner(r.fieldOwner)); err != nil {
1322+
if err := util.UpdateFinalizers(ctx, r.client, crd, r.fieldOwner); err != nil {
13221323
if i == 1 && apierrors.IsConflict(err) {
13231324
log.V(1).Info("error while updating CustomResourcedefinition (409 conflict); doing one retry", "error", err.Error())
13241325
continue
@@ -1343,7 +1344,7 @@ func (r *Reconciler) deleteObject(ctx context.Context, key types.ObjectKey, exis
13431344
}
13441345
if ok := controllerutil.RemoveFinalizer(apiService, r.finalizer); ok {
13451346
// note: 409 error is very likely here (because of concurrent updates happening through the api server); this is why we retry once
1346-
if err := r.client.Update(ctx, apiService, client.FieldOwner(r.fieldOwner)); err != nil {
1347+
if err := util.UpdateFinalizers(ctx, r.client, apiService, r.fieldOwner); err != nil {
13471348
if i == 1 && apierrors.IsConflict(err) {
13481349
log.V(1).Info("error while updating APIService (409 conflict); doing one retry", "error", err.Error())
13491350
continue
@@ -1371,7 +1372,7 @@ func (r *Reconciler) orphanObject(ctx context.Context, existingObject *unstructu
13711372
if isCrd(existingObject) || isApiService(existingObject) {
13721373
object := existingObject.DeepCopy()
13731374
if controllerutil.RemoveFinalizer(object, r.finalizer) {
1374-
if err := r.client.Update(ctx, object, client.FieldOwner(r.fieldOwner)); err != nil {
1375+
if err := util.UpdateFinalizers(ctx, r.client, object, r.fieldOwner); err != nil {
13751376
return err
13761377
}
13771378
}

0 commit comments

Comments
 (0)