• 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

76.56
/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) {
268✔
53
        log := ctrl.LoggerFrom(ctx)
268✔
54
        ctrlResult := ctrl.Result{}
268✔
55

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

268✔
58
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
359✔
59
                return ctrlResult, nil
91✔
60
        }
91✔
61

62
        // Fetch the Release instance
63
        res := &solarv1alpha1.Release{}
177✔
64
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
178✔
65
                if apierrors.IsNotFound(err) {
2✔
66
                        return ctrlResult, nil
1✔
67
                }
1✔
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() {
182✔
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) {
216✔
107
                latest := &solarv1alpha1.Release{}
46✔
108
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
46✔
NEW
109
                        return ctrlResult, errLogAndWrap(log, err, "failed to get latest Release for finalizer addition")
×
NEW
110
                }
×
111
                original := latest.DeepCopy()
46✔
112
                latest.Finalizers = append(latest.Finalizers, releaseFinalizer)
46✔
113
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
46✔
NEW
114
                        return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer to Release")
×
NEW
115
                }
×
116

117
                return ctrlResult, nil
46✔
118
        }
119

120
        cvNamespace := res.Namespace
124✔
121
        if res.Spec.ComponentVersionNamespace != "" {
132✔
122
                cvNamespace = res.Spec.ComponentVersionNamespace
8✔
123
        }
8✔
124

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

145
                        return ctrlResult, nil
4✔
146
                }
147
        }
148

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

170
                        return ctrlResult, nil
33✔
171
                }
172

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

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

185
        // ComponentVersion found — set resolved condition and effective unique name.
186
        uname := effectiveUniqueName(res, cv)
87✔
187

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

203
        return ctrlResult, nil
82✔
204
}
205

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

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

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

NEW
227
                return nil // another active Release still references this ComponentVersion
×
228
        }
229

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

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

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

2✔
246
        return nil
2✔
247
}
248

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

262
        return false, nil
4✔
263
}
264

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

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

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

×
289
                return nil
×
290
        }
×
291

292
        requests := make([]reconcile.Request, len(releaseList.Items))
229✔
293
        for i := range releaseList.Items {
332✔
294
                requests[i] = reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&releaseList.Items[i])}
103✔
295
        }
103✔
296

297
        return requests
229✔
298
}
299

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

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

310
        if !grantsComponentVersionResource(grant) {
20✔
311
                return nil
8✔
312
        }
8✔
313

314
        var requests []reconcile.Request
4✔
315

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

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

335
        return requests
4✔
336
}
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