• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

opendefensecloud / solution-arsenal / 27760471962

18 Jun 2026 12:47PM UTC coverage: 75.441% (-1.1%) from 76.578%
27760471962

Pull #621

github

web-flow
Merge 86594318e into 8ad8e9940
Pull Request #621: feat(controller): implement deletion protection via controller-managed finalizers

416 of 657 new or added lines in 7 files covered. (63.32%)

2 existing lines in 1 file now uncovered.

4021 of 5330 relevant lines covered (75.44%)

33.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.12
/pkg/controller/releasebinding_controller.go
1
// Copyright 2026 BWI GmbH and Solution Arsenal contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "slices"
9

10
        apierrors "k8s.io/apimachinery/pkg/api/errors"
11
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12
        "k8s.io/apimachinery/pkg/runtime"
13
        "k8s.io/apimachinery/pkg/types"
14
        ctrl "sigs.k8s.io/controller-runtime"
15
        "sigs.k8s.io/controller-runtime/pkg/client"
16

17
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
18
)
19

20
// ReleaseBindingReconciler manages the deletion-protection finalizer on the Release referenced
21
// by each ReleaseBinding. This covers both Profile-created and manually created ReleaseBindings.
22
type ReleaseBindingReconciler struct {
23
        client.Client
24
        Scheme *runtime.Scheme
25
        // WatchNamespace restricts reconciliation to this namespace.
26
        // Should be empty in production (watches all namespaces).
27
        // Intended for use in integration tests only.
28
        WatchNamespace string
29
}
30

31
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch;update;patch
32
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings/finalizers,verbs=update
33
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch;update;patch
34
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/finalizers,verbs=update
35
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles,verbs=get;list;watch
36

37
func (r *ReleaseBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
158✔
38
        log := ctrl.LoggerFrom(ctx)
158✔
39

158✔
40
        log.V(1).Info("ReleaseBinding is being reconciled", "req", req)
158✔
41

158✔
42
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
223✔
43
                return ctrl.Result{}, nil
65✔
44
        }
65✔
45

46
        rb := &solarv1alpha1.ReleaseBinding{}
93✔
47
        if err := r.Get(ctx, req.NamespacedName, rb); err != nil {
101✔
48
                if apierrors.IsNotFound(err) {
16✔
49
                        return ctrl.Result{}, nil
8✔
50
                }
8✔
51

NEW
52
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ReleaseBinding")
×
53
        }
54

55
        // Handle deletion: remove releaseRefFinalizer from Release if no other active referencer exists.
56
        if !rb.DeletionTimestamp.IsZero() {
95✔
57
                if rb.Spec.ReleaseRef.Name != "" {
20✔
58
                        // If owned by a Profile that is still managing cleanup (profileFinalizer present),
10✔
59
                        // defer release-ref removal to the Profile controller.
10✔
60
                        profileOwnerManaging := false
10✔
61
                        if ownerRef := metav1.GetControllerOf(rb); ownerRef != nil && ownerRef.Kind == "Profile" && ownerRef.APIVersion == solarv1alpha1.SchemeGroupVersion.String() {
16✔
62
                                ownerProfile := &solarv1alpha1.Profile{}
6✔
63
                                if err := r.Get(ctx, types.NamespacedName{Name: ownerRef.Name, Namespace: rb.Namespace}, ownerProfile); err != nil {
6✔
NEW
64
                                        if !apierrors.IsNotFound(err) {
×
NEW
65
                                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check owner Profile during ReleaseBinding deletion")
×
NEW
66
                                        }
×
67
                                } else if slices.Contains(ownerProfile.Finalizers, profileFinalizer) {
12✔
68
                                        profileOwnerManaging = true
6✔
69
                                }
6✔
70
                        }
71
                        if !profileOwnerManaging {
14✔
72
                                release := &solarv1alpha1.Release{}
4✔
73
                                if err := r.Get(ctx, types.NamespacedName{Name: rb.Spec.ReleaseRef.Name, Namespace: rb.Namespace}, release); err != nil {
4✔
NEW
74
                                        if !apierrors.IsNotFound(err) {
×
NEW
75
                                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release for finalizer cleanup")
×
NEW
76
                                        }
×
77
                                } else if err := r.removeReleaseRefFinalizer(ctx, rb, release); err != nil {
4✔
NEW
78
                                        return ctrl.Result{}, err
×
NEW
79
                                }
×
80
                        }
81
                }
82

83
                if slices.Contains(rb.Finalizers, releaseBindingFinalizer) {
18✔
84
                        latest := &solarv1alpha1.ReleaseBinding{}
8✔
85
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
8✔
NEW
86
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest ReleaseBinding for finalizer removal")
×
NEW
87
                        }
×
88
                        original := latest.DeepCopy()
8✔
89
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool { return s == releaseBindingFinalizer })
18✔
90
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
8✔
NEW
91
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from ReleaseBinding")
×
NEW
92
                        }
×
93
                }
94

95
                return ctrl.Result{}, nil
10✔
96
        }
97

98
        // Ensure self-finalizer exists.
99
        if !slices.Contains(rb.Finalizers, releaseBindingFinalizer) {
112✔
100
                latest := &solarv1alpha1.ReleaseBinding{}
37✔
101
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
37✔
NEW
102
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest ReleaseBinding for finalizer addition")
×
NEW
103
                }
×
104
                if !slices.Contains(latest.Finalizers, releaseBindingFinalizer) {
74✔
105
                        original := latest.DeepCopy()
37✔
106
                        latest.Finalizers = append(latest.Finalizers, releaseBindingFinalizer)
37✔
107
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
37✔
NEW
108
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to ReleaseBinding")
×
NEW
109
                        }
×
110
                }
111
        }
112

113
        // Protect the referenced Release from deletion.
114
        if rb.Spec.ReleaseRef.Name != "" {
150✔
115
                release := &solarv1alpha1.Release{}
75✔
116
                if err := r.Get(ctx, types.NamespacedName{Name: rb.Spec.ReleaseRef.Name, Namespace: rb.Namespace}, release); err != nil {
89✔
117
                        if !apierrors.IsNotFound(err) {
14✔
NEW
118
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release for protection finalizer")
×
NEW
119
                        }
×
120
                } else if !slices.Contains(release.Finalizers, releaseRefFinalizer) {
102✔
121
                        // If this ReleaseBinding is owned by a Profile that is gone or being deleted, skip
41✔
122
                        // adding the protection finalizer — the binding will be GC'd alongside the Profile,
41✔
123
                        // and the Profile controller already handled removal of releaseRefFinalizer.
41✔
124
                        if ownerRef := metav1.GetControllerOf(rb); ownerRef != nil && ownerRef.Kind == "Profile" && ownerRef.APIVersion == solarv1alpha1.SchemeGroupVersion.String() {
41✔
NEW
125
                                ownerProfile := &solarv1alpha1.Profile{}
×
NEW
126
                                err := r.Get(ctx, types.NamespacedName{Name: ownerRef.Name, Namespace: rb.Namespace}, ownerProfile)
×
NEW
127
                                if apierrors.IsNotFound(err) || (err == nil && !ownerProfile.DeletionTimestamp.IsZero()) {
×
NEW
128
                                        return ctrl.Result{}, nil
×
NEW
129
                                }
×
NEW
130
                                if err != nil {
×
NEW
131
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check owner Profile for deletion status")
×
NEW
132
                                }
×
133
                        }
134
                        freshRelease := &solarv1alpha1.Release{}
41✔
135
                        if err := r.Get(ctx, types.NamespacedName{Name: rb.Spec.ReleaseRef.Name, Namespace: rb.Namespace}, freshRelease); err != nil {
41✔
NEW
136
                                if !apierrors.IsNotFound(err) {
×
NEW
137
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Release for finalizer addition")
×
NEW
138
                                }
×
139

NEW
140
                                return ctrl.Result{}, nil
×
141
                        }
142
                        if !slices.Contains(freshRelease.Finalizers, releaseRefFinalizer) {
82✔
143
                                original := freshRelease.DeepCopy()
41✔
144
                                freshRelease.Finalizers = append(freshRelease.Finalizers, releaseRefFinalizer)
41✔
145
                                if err := r.Patch(ctx, freshRelease, client.MergeFromWithOptions(original, client.MergeFromWithOptimisticLock{})); err != nil {
56✔
146
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add protection finalizer to Release")
15✔
147
                                }
15✔
148
                        }
149
                }
150
        }
151

152
        return ctrl.Result{}, nil
60✔
153
}
154

155
// removeReleaseRefFinalizer removes releaseRefFinalizer from release when no other active
156
// Profile or ReleaseBinding (excluding the deleting ReleaseBinding) still references it.
157
func (r *ReleaseBindingReconciler) removeReleaseRefFinalizer(ctx context.Context, deletingRB *solarv1alpha1.ReleaseBinding, release *solarv1alpha1.Release) error {
4✔
158
        if !slices.Contains(release.Finalizers, releaseRefFinalizer) {
4✔
NEW
159
                return nil
×
NEW
160
        }
×
161

162
        // Count active Profiles in the same namespace referencing this Release.
163
        profileList := &solarv1alpha1.ProfileList{}
4✔
164
        if err := r.List(ctx, profileList,
4✔
165
                client.InNamespace(release.Namespace),
4✔
166
                client.MatchingFields{indexProfileByReleaseName: release.Name},
4✔
167
        ); err != nil {
4✔
NEW
168
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to list Profiles for Release finalizer check")
×
NEW
169
        }
×
170

171
        for _, p := range profileList.Items {
4✔
NEW
172
                if !p.DeletionTimestamp.IsZero() {
×
NEW
173
                        // Profile is deleting. If profile-finalizer is still present, the Profile controller
×
NEW
174
                        // is managing cleanup and will remove release-ref once all owned bindings are gone.
×
NEW
175
                        if slices.Contains(p.Finalizers, profileFinalizer) {
×
NEW
176
                                return nil
×
NEW
177
                        }
×
178

NEW
179
                        continue
×
180
                }
181

NEW
182
                return nil
×
183
        }
184

185
        // Count active ReleaseBindings (excluding self and those owned by deleting Profiles) referencing this Release.
186
        bindingList := &solarv1alpha1.ReleaseBindingList{}
4✔
187
        if err := r.List(ctx, bindingList,
4✔
188
                client.InNamespace(release.Namespace),
4✔
189
                client.MatchingFields{indexReleaseBindingReleaseName: release.Name},
4✔
190
        ); err != nil {
4✔
NEW
191
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to list ReleaseBindings for Release finalizer check")
×
NEW
192
        }
×
193

194
        // Cache owner Profile lookups to avoid repeated Gets when bindings share the same owner.
195
        ownerProfileCache := map[string]*solarv1alpha1.Profile{} // nil = gone or deleting
4✔
196
        for _, rb := range bindingList.Items {
9✔
197
                if rb.Name == deletingRB.Name || !rb.DeletionTimestamp.IsZero() {
9✔
198
                        continue
4✔
199
                }
200

201
                ownerRef := metav1.GetControllerOf(&rb)
1✔
202
                if ownerRef == nil || ownerRef.Kind != "Profile" || ownerRef.APIVersion != solarv1alpha1.SchemeGroupVersion.String() {
2✔
203
                        return nil // active binding without a Profile owner → Release still referenced
1✔
204
                }
1✔
205

NEW
206
                cacheKey := rb.Namespace + "/" + ownerRef.Name
×
NEW
207
                if _, seen := ownerProfileCache[cacheKey]; !seen {
×
NEW
208
                        op := &solarv1alpha1.Profile{}
×
NEW
209
                        err := r.Get(ctx, types.NamespacedName{Name: ownerRef.Name, Namespace: rb.Namespace}, op)
×
NEW
210
                        switch {
×
NEW
211
                        case apierrors.IsNotFound(err) || (err == nil && !op.DeletionTimestamp.IsZero()):
×
NEW
212
                                ownerProfileCache[cacheKey] = nil // gone or deleting
×
NEW
213
                        case err != nil:
×
NEW
214
                                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to check owner Profile for Release finalizer check")
×
NEW
215
                        default:
×
NEW
216
                                ownerProfileCache[cacheKey] = op
×
217
                        }
218
                }
219

NEW
220
                if ownerProfileCache[cacheKey] != nil {
×
NEW
221
                        return nil // binding is actively owned → Release still referenced
×
NEW
222
                }
×
223
        }
224

225
        freshRelease := &solarv1alpha1.Release{}
3✔
226
        if err := r.Get(ctx, client.ObjectKeyFromObject(release), freshRelease); err != nil {
3✔
NEW
227
                if apierrors.IsNotFound(err) {
×
NEW
228
                        return nil
×
NEW
229
                }
×
230

NEW
231
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to get latest Release for finalizer removal")
×
232
        }
233
        original := freshRelease.DeepCopy()
3✔
234
        freshRelease.Finalizers = slices.DeleteFunc(freshRelease.Finalizers, func(s string) bool { return s == releaseRefFinalizer })
8✔
235
        if err := r.Patch(ctx, freshRelease, client.MergeFromWithOptions(original, client.MergeFromWithOptimisticLock{})); err != nil {
3✔
NEW
236
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to remove protection finalizer from Release")
×
NEW
237
        }
×
238

239
        ctrl.LoggerFrom(ctx).V(1).Info("Removed protection finalizer from Release", "release", release.Name)
3✔
240

3✔
241
        return nil
3✔
242
}
243

244
// SetupWithManager sets up the controller with the Manager.
245
func (r *ReleaseBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
246
        return ctrl.NewControllerManagedBy(mgr).
1✔
247
                For(&solarv1alpha1.ReleaseBinding{}).
1✔
248
                Complete(r)
1✔
249
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc