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

opendefensecloud / solution-arsenal / 27718445335

17 Jun 2026 08:42PM UTC coverage: 75.301%. First build
27718445335

Pull #621

github

web-flow
Merge ca8a52faa into c1c269b08
Pull Request #621: feat(controller): implement deletion protection via controller-managed finalizers

402 of 641 new or added lines in 7 files covered. (62.71%)

3997 of 5308 relevant lines covered (75.3%)

37.76 hits per line

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

58.55
/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 {
98✔
48
                if apierrors.IsNotFound(err) {
14✔
49
                        return ctrl.Result{}, nil
7✔
50
                }
7✔
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() {
94✔
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) {
111✔
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
                return ctrl.Result{}, nil
37✔
111
        }
112

113
        // Protect the referenced Release from deletion.
114
        if rb.Spec.ReleaseRef.Name != "" {
74✔
115
                release := &solarv1alpha1.Release{}
37✔
116
                if err := r.Get(ctx, types.NamespacedName{Name: rb.Spec.ReleaseRef.Name, Namespace: rb.Namespace}, release); err != nil {
43✔
117
                        if !apierrors.IsNotFound(err) {
6✔
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) {
57✔
121
                        // If this ReleaseBinding is owned by a Profile that is gone or being deleted, skip
26✔
122
                        // adding the protection finalizer — the binding will be GC'd alongside the Profile,
26✔
123
                        // and the Profile controller already handled removal of releaseRefFinalizer.
26✔
124
                        if ownerRef := metav1.GetControllerOf(rb); ownerRef != nil && ownerRef.Kind == "Profile" && ownerRef.APIVersion == solarv1alpha1.SchemeGroupVersion.String() {
26✔
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
                        latest := release.DeepCopy()
26✔
135
                        latest.Finalizers = append(latest.Finalizers, releaseRefFinalizer)
26✔
136
                        if err := r.Patch(ctx, latest, client.MergeFrom(release)); err != nil {
26✔
NEW
137
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to add protection finalizer to Release")
×
NEW
138
                        }
×
139
                }
140
        }
141

142
        return ctrl.Result{}, nil
37✔
143
}
144

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

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

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

NEW
169
                        continue
×
170
                }
171

NEW
172
                return nil
×
173
        }
174

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

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

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

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

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

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

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

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

3✔
231
        return nil
3✔
232
}
233

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