• 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

70.53
/pkg/controller/componentversion_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
        "k8s.io/apimachinery/pkg/runtime"
12
        "k8s.io/apimachinery/pkg/types"
13
        ctrl "sigs.k8s.io/controller-runtime"
14
        "sigs.k8s.io/controller-runtime/pkg/client"
15

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

19
// ComponentVersionReconciler manages the deletion-protection finalizer on the Component
20
// referenced by each ComponentVersion, preventing Component deletion while ComponentVersions exist.
21
type ComponentVersionReconciler struct {
22
        client.Client
23
        Scheme *runtime.Scheme
24
        // WatchNamespace restricts reconciliation to this namespace.
25
        // Should be empty in production (watches all namespaces).
26
        // Intended for use in integration tests only.
27
        WatchNamespace string
28
}
29

30
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch;update;patch
31
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions/finalizers,verbs=update
32
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=components,verbs=get;list;watch;update;patch
33
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=components/finalizers,verbs=update
34

35
func (r *ComponentVersionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
145✔
36
        log := ctrl.LoggerFrom(ctx)
145✔
37

145✔
38
        log.V(1).Info("ComponentVersion is being reconciled", "req", req)
145✔
39

145✔
40
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
202✔
41
                return ctrl.Result{}, nil
57✔
42
        }
57✔
43

44
        cv := &solarv1alpha1.ComponentVersion{}
88✔
45
        if err := r.Get(ctx, req.NamespacedName, cv); err != nil {
92✔
46
                if apierrors.IsNotFound(err) {
8✔
47
                        return ctrl.Result{}, nil
4✔
48
                }
4✔
49

NEW
50
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
51
        }
52

53
        // Handle deletion: remove componentRefFinalizer from Component if no other CV references it.
54
        if !cv.DeletionTimestamp.IsZero() {
89✔
55
                if cv.Spec.ComponentRef.Name != "" {
10✔
56
                        comp := &solarv1alpha1.Component{}
5✔
57
                        if err := r.Get(ctx, types.NamespacedName{Name: cv.Spec.ComponentRef.Name, Namespace: cv.Namespace}, comp); err != nil {
7✔
58
                                if !apierrors.IsNotFound(err) {
2✔
NEW
59
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Component for finalizer cleanup")
×
NEW
60
                                }
×
61
                        } else if err := r.removeComponentRefFinalizer(ctx, cv, comp); err != nil {
3✔
NEW
62
                                return ctrl.Result{}, err
×
NEW
63
                        }
×
64
                }
65

66
                if slices.Contains(cv.Finalizers, componentVersionFinalizer) {
9✔
67
                        latest := &solarv1alpha1.ComponentVersion{}
4✔
68
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
4✔
NEW
69
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest ComponentVersion for finalizer removal")
×
NEW
70
                        }
×
71
                        original := latest.DeepCopy()
4✔
72
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool { return s == componentVersionFinalizer })
9✔
73
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
4✔
NEW
74
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from ComponentVersion")
×
NEW
75
                        }
×
76
                }
77

78
                return ctrl.Result{}, nil
5✔
79
        }
80

81
        // Ensure self-finalizer exists.
82
        if !slices.Contains(cv.Finalizers, componentVersionFinalizer) {
107✔
83
                latest := &solarv1alpha1.ComponentVersion{}
28✔
84
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
28✔
NEW
85
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest ComponentVersion for finalizer addition")
×
NEW
86
                }
×
87
                if !slices.Contains(latest.Finalizers, componentVersionFinalizer) {
56✔
88
                        original := latest.DeepCopy()
28✔
89
                        latest.Finalizers = append(latest.Finalizers, componentVersionFinalizer)
28✔
90
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
28✔
NEW
91
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to ComponentVersion")
×
NEW
92
                        }
×
93
                }
94
        }
95

96
        // Protect the referenced Component from deletion.
97
        if cv.Spec.ComponentRef.Name != "" {
158✔
98
                comp := &solarv1alpha1.Component{}
79✔
99
                if err := r.Get(ctx, types.NamespacedName{Name: cv.Spec.ComponentRef.Name, Namespace: cv.Namespace}, comp); err != nil {
148✔
100
                        if !apierrors.IsNotFound(err) {
69✔
NEW
101
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Component for protection finalizer")
×
NEW
102
                        }
×
103
                } else if !slices.Contains(comp.Finalizers, componentRefFinalizer) {
14✔
104
                        latest := comp.DeepCopy()
4✔
105
                        latest.Finalizers = append(latest.Finalizers, componentRefFinalizer)
4✔
106
                        if err := r.Patch(ctx, latest, client.MergeFrom(comp)); err != nil {
4✔
NEW
107
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to add protection finalizer to Component")
×
NEW
108
                        }
×
109
                }
110
        }
111

112
        return ctrl.Result{}, nil
79✔
113
}
114

115
// removeComponentRefFinalizer removes componentRefFinalizer from comp when no other active
116
// ComponentVersion still references it (excluding the CV currently being deleted).
117
func (r *ComponentVersionReconciler) removeComponentRefFinalizer(ctx context.Context, deletingCV *solarv1alpha1.ComponentVersion, comp *solarv1alpha1.Component) error {
3✔
118
        if !slices.Contains(comp.Finalizers, componentRefFinalizer) {
3✔
NEW
119
                return nil
×
NEW
120
        }
×
121

122
        cvList := &solarv1alpha1.ComponentVersionList{}
3✔
123
        if err := r.List(ctx, cvList,
3✔
124
                client.InNamespace(comp.Namespace),
3✔
125
                client.MatchingFields{indexCVByComponentName: comp.Name},
3✔
126
        ); err != nil {
3✔
NEW
127
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to list ComponentVersions for Component finalizer check")
×
NEW
128
        }
×
129

130
        for _, cv := range cvList.Items {
7✔
131
                if cv.Name == deletingCV.Name {
7✔
132
                        continue
3✔
133
                }
134
                if !cv.DeletionTimestamp.IsZero() {
1✔
NEW
135
                        continue
×
136
                }
137

138
                return nil // another active ComponentVersion still references this Component
1✔
139
        }
140

141
        freshComp := &solarv1alpha1.Component{}
2✔
142
        if err := r.Get(ctx, client.ObjectKeyFromObject(comp), freshComp); err != nil {
2✔
NEW
143
                if apierrors.IsNotFound(err) {
×
NEW
144
                        return nil
×
NEW
145
                }
×
146

NEW
147
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to get latest Component for finalizer removal")
×
148
        }
149
        original := freshComp.DeepCopy()
2✔
150
        freshComp.Finalizers = slices.DeleteFunc(freshComp.Finalizers, func(s string) bool { return s == componentRefFinalizer })
4✔
151
        if err := r.Patch(ctx, freshComp, client.MergeFrom(original)); err != nil {
2✔
NEW
152
                return errLogAndWrap(ctrl.LoggerFrom(ctx), err, "failed to remove protection finalizer from Component")
×
NEW
153
        }
×
154

155
        ctrl.LoggerFrom(ctx).V(1).Info("Removed protection finalizer from Component", "component", comp.Name)
2✔
156

2✔
157
        return nil
2✔
158
}
159

160
// SetupWithManager sets up the controller with the Manager.
161
func (r *ComponentVersionReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
162
        return ctrl.NewControllerManagedBy(mgr).
1✔
163
                For(&solarv1alpha1.ComponentVersion{}).
1✔
164
                Complete(r)
1✔
165
}
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