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

opendefensecloud / solution-arsenal / 27755794823

18 Jun 2026 11:18AM UTC coverage: 75.292% (-1.7%) from 77.003%
27755794823

Pull #621

github

web-flow
Merge 3e94622f3 into 69595b7d1
Pull Request #621: feat(controller): implement deletion protection via controller-managed finalizers

397 of 636 new or added lines in 7 files covered. (62.42%)

15 existing lines in 3 files now uncovered.

4001 of 5314 relevant lines covered (75.29%)

33.24 hits per line

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

58.28
/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) {
157✔
38
        log := ctrl.LoggerFrom(ctx)
157✔
39

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

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

46
        rb := &solarv1alpha1.ReleaseBinding{}
91✔
47
        if err := r.Get(ctx, req.NamespacedName, rb); err != nil {
99✔
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() {
93✔
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) {
110✔
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
                original := latest.DeepCopy()
37✔
105
                latest.Finalizers = append(latest.Finalizers, releaseBindingFinalizer)
37✔
106
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
37✔
NEW
107
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to ReleaseBinding")
×
NEW
108
                }
×
109
        }
110

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

140
        return ctrl.Result{}, nil
73✔
141
}
142

143
// removeReleaseRefFinalizer removes releaseRefFinalizer from release when no other active
144
// Profile or ReleaseBinding (excluding the deleting ReleaseBinding) still references it.
145
func (r *ReleaseBindingReconciler) removeReleaseRefFinalizer(ctx context.Context, deletingRB *solarv1alpha1.ReleaseBinding, release *solarv1alpha1.Release) error {
4✔
146
        if !slices.Contains(release.Finalizers, releaseRefFinalizer) {
4✔
NEW
147
                return nil
×
NEW
148
        }
×
149

150
        // Count active Profiles in the same namespace referencing this Release.
151
        profileList := &solarv1alpha1.ProfileList{}
4✔
152
        if err := r.List(ctx, profileList,
4✔
153
                client.InNamespace(release.Namespace),
4✔
154
                client.MatchingFields{indexProfileByReleaseName: release.Name},
4✔
155
        ); err != nil {
4✔
NEW
156
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to list Profiles for Release finalizer check")
×
NEW
157
        }
×
158

159
        for _, p := range profileList.Items {
4✔
NEW
160
                if !p.DeletionTimestamp.IsZero() {
×
NEW
161
                        // Profile is deleting. If profile-finalizer is still present, the Profile controller
×
NEW
162
                        // is managing cleanup and will remove release-ref once all owned bindings are gone.
×
NEW
163
                        if slices.Contains(p.Finalizers, profileFinalizer) {
×
NEW
164
                                return nil
×
NEW
165
                        }
×
166

NEW
167
                        continue
×
168
                }
169

NEW
170
                return nil
×
171
        }
172

173
        // Count active ReleaseBindings (excluding self and those owned by deleting Profiles) referencing this Release.
174
        bindingList := &solarv1alpha1.ReleaseBindingList{}
4✔
175
        if err := r.List(ctx, bindingList,
4✔
176
                client.InNamespace(release.Namespace),
4✔
177
                client.MatchingFields{indexReleaseBindingReleaseName: release.Name},
4✔
178
        ); err != nil {
4✔
NEW
179
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to list ReleaseBindings for Release finalizer check")
×
NEW
180
        }
×
181

182
        // Cache owner Profile lookups to avoid repeated Gets when bindings share the same owner.
183
        ownerProfileCache := map[string]*solarv1alpha1.Profile{} // nil = gone or deleting
4✔
184
        for _, rb := range bindingList.Items {
9✔
185
                if rb.Name == deletingRB.Name || !rb.DeletionTimestamp.IsZero() {
9✔
186
                        continue
4✔
187
                }
188

189
                ownerRef := metav1.GetControllerOf(&rb)
1✔
190
                if ownerRef == nil || ownerRef.Kind != "Profile" || ownerRef.APIVersion != solarv1alpha1.SchemeGroupVersion.String() {
2✔
191
                        return nil // active binding without a Profile owner → Release still referenced
1✔
192
                }
1✔
193

NEW
194
                cacheKey := rb.Namespace + "/" + ownerRef.Name
×
NEW
195
                if _, seen := ownerProfileCache[cacheKey]; !seen {
×
NEW
196
                        op := &solarv1alpha1.Profile{}
×
NEW
197
                        err := r.Get(ctx, types.NamespacedName{Name: ownerRef.Name, Namespace: rb.Namespace}, op)
×
NEW
198
                        switch {
×
NEW
199
                        case apierrors.IsNotFound(err) || (err == nil && !op.DeletionTimestamp.IsZero()):
×
NEW
200
                                ownerProfileCache[cacheKey] = nil // gone or deleting
×
NEW
201
                        case err != nil:
×
NEW
202
                                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to check owner Profile for Release finalizer check")
×
NEW
203
                        default:
×
NEW
204
                                ownerProfileCache[cacheKey] = op
×
205
                        }
206
                }
207

NEW
208
                if ownerProfileCache[cacheKey] != nil {
×
NEW
209
                        return nil // binding is actively owned → Release still referenced
×
NEW
210
                }
×
211
        }
212

213
        freshRelease := &solarv1alpha1.Release{}
3✔
214
        if err := r.Get(ctx, client.ObjectKeyFromObject(release), freshRelease); err != nil {
3✔
NEW
215
                if apierrors.IsNotFound(err) {
×
NEW
216
                        return nil
×
NEW
217
                }
×
218

NEW
219
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to get latest Release for finalizer removal")
×
220
        }
221
        original := freshRelease.DeepCopy()
3✔
222
        freshRelease.Finalizers = slices.DeleteFunc(freshRelease.Finalizers, func(s string) bool { return s == releaseRefFinalizer })
8✔
223
        if err := r.Patch(ctx, freshRelease, client.MergeFrom(original)); err != nil {
3✔
NEW
224
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to remove protection finalizer from Release")
×
NEW
225
        }
×
226

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

3✔
229
        return nil
3✔
230
}
231

232
// SetupWithManager sets up the controller with the Manager.
233
func (r *ReleaseBindingReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
234
        return ctrl.NewControllerManagedBy(mgr).
1✔
235
                For(&solarv1alpha1.ReleaseBinding{}).
1✔
236
                Complete(r)
1✔
237
}
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