• 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

78.37
/pkg/controller/release_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
        apimeta "k8s.io/apimachinery/pkg/api/meta"
12
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13
        "k8s.io/apimachinery/pkg/runtime"
14
        "k8s.io/apimachinery/pkg/types"
15
        "k8s.io/client-go/tools/events"
16
        ctrl "sigs.k8s.io/controller-runtime"
17
        "sigs.k8s.io/controller-runtime/pkg/client"
18
        "sigs.k8s.io/controller-runtime/pkg/handler"
19
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
20

21
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
22
)
23

24
const (
25
        ConditionTypeComponentVersionResolved = "ComponentVersionResolved"
26
)
27

28
// ReleaseReconciler reconciles a Release object.
29
// It validates that the referenced ComponentVersion exists and sets status conditions.
30
// Rendering is handled by the Target controller.
31
type ReleaseReconciler struct {
32
        client.Client
33
        Scheme   *runtime.Scheme
34
        Recorder events.EventRecorder
35
        // WatchNamespace restricts reconciliation to this namespace.
36
        // Should be empty in production (watches all namespaces).
37
        // Intended for use in integration tests only.
38
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
39
        WatchNamespace string
40
}
41

42
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch;create;update;patch;delete
43
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/status,verbs=get;update;patch
44
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/finalizers,verbs=update
45
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch;update;patch
46
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions/finalizers,verbs=update
47
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list;watch
48
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
49

50
// Reconcile validates the Release by resolving its ComponentVersion reference and
51
// manages deletion-protection finalizers on that ComponentVersion.
52
func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
256✔
53
        log := ctrl.LoggerFrom(ctx)
256✔
54
        ctrlResult := ctrl.Result{}
256✔
55

256✔
56
        log.V(1).Info("Release is being reconciled", "req", req)
256✔
57

256✔
58
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
344✔
59
                return ctrlResult, nil
88✔
60
        }
88✔
61

62
        // Fetch the Release instance
63
        res := &solarv1alpha1.Release{}
168✔
64
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
171✔
65
                if apierrors.IsNotFound(err) {
6✔
66
                        return ctrlResult, nil
3✔
67
                }
3✔
68

69
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
70
        }
71

72
        // Handle deletion: remove componentVersionRefFinalizer from CV if no other Release references it.
73
        if !res.DeletionTimestamp.IsZero() {
171✔
74
                cvNamespace := res.Namespace
6✔
75
                if res.Spec.ComponentVersionNamespace != "" {
6✔
NEW
76
                        cvNamespace = res.Spec.ComponentVersionNamespace
×
NEW
77
                }
×
78

79
                if res.Spec.ComponentVersionRef.Name != "" {
12✔
80
                        cv := &solarv1alpha1.ComponentVersion{}
6✔
81
                        if err := r.Get(ctx, types.NamespacedName{Name: res.Spec.ComponentVersionRef.Name, Namespace: cvNamespace}, cv); err != nil {
10✔
82
                                if !apierrors.IsNotFound(err) {
4✔
NEW
83
                                        return ctrlResult, errLogAndWrap(log, err, "failed to get ComponentVersion for finalizer cleanup")
×
NEW
84
                                }
×
85
                        } else if err := r.removeComponentVersionRefFinalizer(ctx, res, cv); err != nil {
2✔
NEW
86
                                return ctrlResult, err
×
NEW
87
                        }
×
88
                }
89

90
                if slices.Contains(res.Finalizers, releaseFinalizer) {
10✔
91
                        latest := &solarv1alpha1.Release{}
4✔
92
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
4✔
NEW
93
                                return ctrlResult, errLogAndWrap(log, err, "failed to get latest Release for finalizer removal")
×
NEW
94
                        }
×
95
                        original := latest.DeepCopy()
4✔
96
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool { return s == releaseFinalizer })
10✔
97
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
4✔
NEW
98
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer from Release")
×
NEW
99
                        }
×
100
                }
101

102
                return ctrlResult, nil
6✔
103
        }
104

105
        // Ensure self-finalizer exists before any other work.
106
        if !slices.Contains(res.Finalizers, releaseFinalizer) {
203✔
107
                latest := &solarv1alpha1.Release{}
44✔
108
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
44✔
NEW
109
                        return ctrlResult, errLogAndWrap(log, err, "failed to get latest Release for finalizer addition")
×
NEW
110
                }
×
111
                original := latest.DeepCopy()
44✔
112
                latest.Finalizers = append(latest.Finalizers, releaseFinalizer)
44✔
113
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
44✔
NEW
114
                        return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer to Release")
×
NEW
115
                }
×
116
        }
117

118
        cvNamespace := res.Namespace
159✔
119
        if res.Spec.ComponentVersionNamespace != "" {
170✔
120
                cvNamespace = res.Spec.ComponentVersionNamespace
11✔
121
        }
11✔
122

123
        // For cross-namespace references, verify a ReferenceGrant permits it.
124
        if cvNamespace != res.Namespace {
170✔
125
                granted, err := r.componentVersionGranted(ctx, res, cvNamespace)
11✔
126
                if err != nil {
11✔
127
                        return ctrlResult, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
128
                }
×
129
                if !granted {
16✔
130
                        changed := apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
5✔
131
                                Type:               ConditionTypeComponentVersionResolved,
5✔
132
                                Status:             metav1.ConditionFalse,
5✔
133
                                ObservedGeneration: res.Generation,
5✔
134
                                Reason:             "NotGranted",
5✔
135
                                Message:            "no ReferenceGrant permits access to ComponentVersion in namespace " + cvNamespace,
5✔
136
                        })
5✔
137
                        if changed {
8✔
138
                                if err := r.Status().Update(ctx, res); err != nil {
4✔
139
                                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
1✔
140
                                }
1✔
141
                        }
142

143
                        return ctrlResult, nil
4✔
144
                }
145
        }
146

147
        // Resolve ComponentVersion
148
        cvRef := types.NamespacedName{
154✔
149
                Name:      res.Spec.ComponentVersionRef.Name,
154✔
150
                Namespace: cvNamespace,
154✔
151
        }
154✔
152
        cv := &solarv1alpha1.ComponentVersion{}
154✔
153
        if err := r.Get(ctx, cvRef, cv); err != nil {
200✔
154
                if apierrors.IsNotFound(err) {
92✔
155
                        changed := apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
46✔
156
                                Type:               ConditionTypeComponentVersionResolved,
46✔
157
                                Status:             metav1.ConditionFalse,
46✔
158
                                ObservedGeneration: res.Generation,
46✔
159
                                Reason:             "NotFound",
46✔
160
                                Message:            "ComponentVersion not found: " + res.Spec.ComponentVersionRef.Name,
46✔
161
                        })
46✔
162
                        if changed {
72✔
163
                                if err := r.Status().Update(ctx, res); err != nil {
42✔
164
                                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
16✔
165
                                }
16✔
166
                        }
167

168
                        return ctrlResult, nil
30✔
169
                }
170

171
                return ctrlResult, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
172
        }
173

174
        // Protect ComponentVersion from deletion while this Release references it.
175
        if !slices.Contains(cv.Finalizers, componentVersionRefFinalizer) {
133✔
176
                latest := cv.DeepCopy()
25✔
177
                latest.Finalizers = append(latest.Finalizers, componentVersionRefFinalizer)
25✔
178
                if err := r.Patch(ctx, latest, client.MergeFrom(cv)); err != nil {
25✔
NEW
179
                        return ctrlResult, errLogAndWrap(log, err, "failed to add protection finalizer to ComponentVersion")
×
NEW
180
                }
×
181
        }
182

183
        // ComponentVersion found — set resolved condition and effective unique name.
184
        uname := effectiveUniqueName(res, cv)
108✔
185

108✔
186
        condChanged := apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
108✔
187
                Type:               ConditionTypeComponentVersionResolved,
108✔
188
                Status:             metav1.ConditionTrue,
108✔
189
                ObservedGeneration: res.Generation,
108✔
190
                Reason:             "Resolved",
108✔
191
                Message:            "ComponentVersion resolved: " + cv.Name,
108✔
192
        })
108✔
193
        nameChanged := res.Status.EffectiveUniqueName != uname
108✔
194
        if condChanged || nameChanged {
175✔
195
                res.Status.EffectiveUniqueName = uname
67✔
196
                if err := r.Status().Update(ctx, res); err != nil {
104✔
197
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
37✔
198
                }
37✔
199
        }
200

201
        return ctrlResult, nil
71✔
202
}
203

204
// removeComponentVersionRefFinalizer removes componentVersionRefFinalizer from cv when no other
205
// active Release still references it (excluding the Release that is currently being deleted).
206
func (r *ReleaseReconciler) removeComponentVersionRefFinalizer(ctx context.Context, deletingRelease *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion) error {
2✔
207
        if !slices.Contains(cv.Finalizers, componentVersionRefFinalizer) {
2✔
NEW
208
                return nil
×
NEW
209
        }
×
210

211
        refKey := cv.Namespace + "/" + cv.Name
2✔
212
        releaseList := &solarv1alpha1.ReleaseList{}
2✔
213
        if err := r.List(ctx, releaseList, client.MatchingFields{indexReleaseByCVRef: refKey}); err != nil {
2✔
NEW
214
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to list Releases for ComponentVersion finalizer check")
×
NEW
215
        }
×
216

217
        for _, rel := range releaseList.Items {
4✔
218
                if rel.Name == deletingRelease.Name && rel.Namespace == deletingRelease.Namespace {
4✔
219
                        continue
2✔
220
                }
NEW
221
                if !rel.DeletionTimestamp.IsZero() {
×
NEW
222
                        continue
×
223
                }
224

NEW
225
                return nil // another active Release still references this ComponentVersion
×
226
        }
227

228
        freshCV := &solarv1alpha1.ComponentVersion{}
2✔
229
        if err := r.Get(ctx, client.ObjectKeyFromObject(cv), freshCV); err != nil {
2✔
NEW
230
                if apierrors.IsNotFound(err) {
×
NEW
231
                        return nil
×
NEW
232
                }
×
233

NEW
234
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to get latest ComponentVersion for finalizer removal")
×
235
        }
236
        original := freshCV.DeepCopy()
2✔
237
        freshCV.Finalizers = slices.DeleteFunc(freshCV.Finalizers, func(s string) bool { return s == componentVersionRefFinalizer })
5✔
238
        if err := r.Patch(ctx, freshCV, client.MergeFrom(original)); err != nil {
2✔
NEW
239
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to remove protection finalizer from ComponentVersion")
×
NEW
240
        }
×
241

242
        ctrl.LoggerFrom(ctx).V(1).Info("Removed protection finalizer from ComponentVersion", "componentversion", cv.Name)
2✔
243

2✔
244
        return nil
2✔
245
}
246

247
// componentVersionGranted returns true if a ReferenceGrant in cvNamespace permits
248
// the given Release to reference a ComponentVersion there.
249
func (r *ReleaseReconciler) componentVersionGranted(ctx context.Context, release *solarv1alpha1.Release, cvNamespace string) (bool, error) {
11✔
250
        grantList := &solarv1alpha1.ReferenceGrantList{}
11✔
251
        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
11✔
252
                return false, err
×
253
        }
×
254
        for i := range grantList.Items {
17✔
255
                if grantPermitsComponentVersionAccess(&grantList.Items[i], release.Namespace) {
12✔
256
                        return true, nil
6✔
257
                }
6✔
258
        }
259

260
        return false, nil
5✔
261
}
262

263
// SetupWithManager sets up the controller with the Manager.
264
func (r *ReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
265
        return ctrl.NewControllerManagedBy(mgr).
1✔
266
                For(&solarv1alpha1.Release{}).
1✔
267
                Watches(
1✔
268
                        &solarv1alpha1.ComponentVersion{},
1✔
269
                        handler.EnqueueRequestsFromMapFunc(r.mapComponentVersionToReleases),
1✔
270
                ).
1✔
271
                Watches(
1✔
272
                        &solarv1alpha1.ReferenceGrant{},
1✔
273
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToReleases),
1✔
274
                ).
1✔
275
                Complete(r)
1✔
276
}
1✔
277

278
// mapComponentVersionToReleases enqueues all Releases that reference this ComponentVersion.
279
func (r *ReleaseReconciler) mapComponentVersionToReleases(ctx context.Context, obj client.Object) []reconcile.Request {
229✔
280
        log := ctrl.LoggerFrom(ctx)
229✔
281

229✔
282
        refKey := obj.GetNamespace() + "/" + obj.GetName()
229✔
283
        releaseList := &solarv1alpha1.ReleaseList{}
229✔
284
        if err := r.List(ctx, releaseList, client.MatchingFields{indexReleaseByCVRef: refKey}); err != nil {
229✔
285
                log.Error(err, "failed to list Releases for ComponentVersion mapping")
×
286

×
287
                return nil
×
288
        }
×
289

290
        requests := make([]reconcile.Request, len(releaseList.Items))
229✔
291
        for i := range releaseList.Items {
336✔
292
                requests[i] = reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&releaseList.Items[i])}
107✔
293
        }
107✔
294

295
        return requests
229✔
296
}
297

298
// mapReferenceGrantToReleases enqueues Releases whose cross-namespace ComponentVersion
299
// reference is covered by the changed ReferenceGrant.
300
func (r *ReleaseReconciler) mapReferenceGrantToReleases(ctx context.Context, obj client.Object) []reconcile.Request {
12✔
301
        log := ctrl.LoggerFrom(ctx)
12✔
302

12✔
303
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
12✔
304
        if !ok {
12✔
305
                return nil
×
306
        }
×
307

308
        if !grantsComponentVersionResource(grant) {
20✔
309
                return nil
8✔
310
        }
8✔
311

312
        var requests []reconcile.Request
4✔
313

4✔
314
        for _, from := range grant.Spec.From {
8✔
315
                if from.Kind != "Release" || from.Group != solarGroup {
4✔
316
                        continue
×
317
                }
318
                releaseList := &solarv1alpha1.ReleaseList{}
4✔
319
                if err := r.List(ctx, releaseList, client.InNamespace(from.Namespace)); err != nil {
4✔
320
                        log.Error(err, "failed to list Releases for ReferenceGrant mapping", "namespace", from.Namespace)
×
321

×
322
                        continue
×
323
                }
324
                for _, rel := range releaseList.Items {
5✔
325
                        if rel.Spec.ComponentVersionNamespace == grant.Namespace {
2✔
326
                                requests = append(requests, reconcile.Request{
1✔
327
                                        NamespacedName: client.ObjectKeyFromObject(&rel),
1✔
328
                                })
1✔
329
                        }
1✔
330
                }
331
        }
332

333
        return requests
4✔
334
}
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