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

opendefensecloud / solution-arsenal / 23050779692

13 Mar 2026 12:26PM UTC coverage: 71.095% (-0.9%) from 72.012%
23050779692

Pull #273

github

web-flow
Merge 418948af2 into b27e7dfd8
Pull Request #273: Target e2e test

5 of 7 new or added lines in 2 files covered. (71.43%)

82 existing lines in 8 files now uncovered.

2098 of 2951 relevant lines covered (71.09%)

18.61 hits per line

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

76.05
/pkg/controller/target_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
        "fmt"
9
        "slices"
10

11
        corev1 "k8s.io/api/core/v1"
12
        apiequality "k8s.io/apimachinery/pkg/api/equality"
13
        apierrors "k8s.io/apimachinery/pkg/api/errors"
14
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15
        "k8s.io/apimachinery/pkg/labels"
16
        "k8s.io/apimachinery/pkg/runtime"
17
        "k8s.io/apimachinery/pkg/types"
18
        "k8s.io/client-go/tools/events"
19
        ctrl "sigs.k8s.io/controller-runtime"
20
        "sigs.k8s.io/controller-runtime/pkg/builder"
21
        "sigs.k8s.io/controller-runtime/pkg/client"
22
        "sigs.k8s.io/controller-runtime/pkg/event"
23
        "sigs.k8s.io/controller-runtime/pkg/handler"
24
        "sigs.k8s.io/controller-runtime/pkg/predicate"
25
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
26

27
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
28
)
29

30
const (
31
        targetFinalizer = "solar.opendefense.cloud/target-finalizer"
32
)
33

34
type TargetReconciler struct {
35
        client.Client
36
        Scheme   *runtime.Scheme
37
        Recorder events.EventRecorder
38
}
39

40
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch
41
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update
42
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete
43
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets,verbs=get;list;watch;create;update;patch;delete
44
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles,verbs=get;list;watch
45
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
46
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
47

48
// Reconcile moves the current state of the cluster closer to the desired state
49
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
77✔
50
        log := ctrl.LoggerFrom(ctx)
77✔
51
        ctrlResult := ctrl.Result{}
77✔
52

77✔
53
        log.V(1).Info("Target is being reconciled", "req", req)
77✔
54

77✔
55
        // Fetch target
77✔
56
        target := &solarv1alpha1.Target{}
77✔
57
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
78✔
58
                if apierrors.IsNotFound(err) {
2✔
59
                        return ctrlResult, nil
1✔
60
                }
1✔
61

62
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
63
        }
64

65
        // Handle deletion
66
        if !target.DeletionTimestamp.IsZero() {
77✔
67
                log.V(1).Info("Target is being deleted")
1✔
68
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up HydratedTarget")
1✔
69

1✔
70
                // Delete HydratedTarget
1✔
71
                if err := r.Delete(ctx, &solarv1alpha1.HydratedTarget{ObjectMeta: metav1.ObjectMeta{Namespace: target.Namespace, Name: target.Name}}); err != nil && !apierrors.IsNotFound(err) {
1✔
72
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete HydratedTarget")
×
73
                }
×
74

75
                // Remove finalizer
76
                if slices.Contains(target.Finalizers, targetFinalizer) {
2✔
77
                        // Re-fetch latest version to avoid conflicts
1✔
78
                        latest := &solarv1alpha1.Target{}
1✔
79
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
1✔
UNCOV
80
                                return ctrlResult, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
UNCOV
81
                        }
×
82
                        log.V(1).Info("Removing finalizer from Target")
1✔
83
                        original := latest.DeepCopy()
1✔
84
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
2✔
85
                                return s == targetFinalizer
1✔
86
                        })
1✔
87

88
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
1✔
89
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
90
                        }
×
91
                }
92

93
                return ctrlResult, nil
1✔
94
        }
95

96
        // Set finalizer if not set already and not currently deleting
97
        if target.DeletionTimestamp.IsZero() && !slices.Contains(target.Finalizers, targetFinalizer) {
87✔
98
                log.V(1).Info("Target does not have finalizer set, adding finalizer")
12✔
99
                latest := &solarv1alpha1.Target{}
12✔
100
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
12✔
101
                        return ctrlResult, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
102
                }
×
103
                original := latest.DeepCopy()
12✔
104
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
12✔
105
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
12✔
106
                        return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
107
                }
×
108

109
                return ctrlResult, nil
12✔
110
        }
111

112
        // Get matching profiles
113
        profileList := &solarv1alpha1.ProfileList{}
63✔
114
        if err := r.List(ctx, profileList, client.InNamespace(target.Namespace)); err != nil {
63✔
115
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list Profiles")
×
116
        }
×
117

118
        matchingProfiles := make(map[string]corev1.LocalObjectReference)
63✔
119
        targetLabels := labels.Set(target.Labels)
63✔
120

63✔
121
        for _, profile := range profileList.Items {
106✔
122
                selector, err := metav1.LabelSelectorAsSelector(&profile.Spec.TargetSelector)
43✔
123
                if err != nil {
43✔
124
                        log.Error(err, "invalid targetSelector in Profile; skipping")
×
125
                        continue
×
126
                }
127

128
                if selector.Matches(targetLabels) {
74✔
129
                        matchingProfiles[profile.Name] = corev1.LocalObjectReference{Name: profile.Name}
31✔
130
                }
31✔
131
        }
132

133
        // Check if hydrated target exists, if not create and make sure to SetControllerReference...
134
        hydratedTarget := &solarv1alpha1.HydratedTarget{}
63✔
135
        err := r.Get(ctx, req.NamespacedName, hydratedTarget)
63✔
136

63✔
137
        if err != nil && !apierrors.IsNotFound(err) {
63✔
138
                return ctrlResult, errLogAndWrap(log, err, "failed to get HydratedTarget")
×
139
        }
×
140

141
        // Create HydratedTarget if not exists or update/override spec
142
        if apierrors.IsNotFound(err) {
75✔
143
                log.V(1).Info("Creating HydratedTarget for Target", "target", req.NamespacedName)
12✔
144
                hydratedTarget = &solarv1alpha1.HydratedTarget{
12✔
145
                        ObjectMeta: metav1.ObjectMeta{
12✔
146
                                Name:      target.Name,
12✔
147
                                Namespace: target.Namespace,
12✔
148
                        },
12✔
149
                        Spec: solarv1alpha1.HydratedTargetSpec{
12✔
150
                                Releases: target.Spec.Releases,
12✔
151
                                Profiles: matchingProfiles,
12✔
152
                                Userdata: target.Spec.Userdata,
12✔
153
                        },
12✔
154
                }
12✔
155
                if err := ctrl.SetControllerReference(target, hydratedTarget, r.Scheme); err != nil {
12✔
156
                        return ctrlResult, errLogAndWrap(log, err, "failed to set controller reference on HydratedTarget")
×
157
                }
×
158
                if err := r.Create(ctx, hydratedTarget); err != nil {
12✔
159
                        if !apierrors.IsAlreadyExists(err) {
×
160
                                return ctrlResult, errLogAndWrap(log, err, "failed to create HydratedTarget")
×
161
                        }
×
162
                        log.V(1).Info("HydratedTarget already exists, will update", "hydratedTarget", req.NamespacedName)
×
163
                } else {
12✔
164
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create", "Created HydratedTarget %s/%s", hydratedTarget.Namespace, hydratedTarget.Name)
12✔
165
                        return ctrlResult, nil
12✔
166
                }
12✔
167
        }
168

169
        // Update if out of sync
170
        // re-fetch target and hydratedTarget to avoid conflicts
171
        hydratedTarget = &solarv1alpha1.HydratedTarget{}
51✔
172
        if err := r.Get(ctx, req.NamespacedName, hydratedTarget); err != nil {
51✔
173
                return ctrlResult, errLogAndWrap(log, err, "failed to re-fetch HydratedTarget for update check")
×
174
        }
×
175
        target = &solarv1alpha1.Target{}
51✔
176
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
51✔
177
                return ctrlResult, errLogAndWrap(log, err, "failed to re-fetch Target for update check")
×
178
        }
×
179

180
        original := hydratedTarget.DeepCopy()
51✔
181

51✔
182
        hydratedTarget.Spec.Releases = target.Spec.Releases
51✔
183
        hydratedTarget.Spec.Profiles = matchingProfiles
51✔
184
        hydratedTarget.Spec.Userdata = target.Spec.Userdata
51✔
185

51✔
186
        if !apiequality.Semantic.DeepEqual(original.Spec, hydratedTarget.Spec) {
59✔
187
                log.V(1).Info("Updating HydratedTarget for Target", "target", req.NamespacedName)
8✔
188
                if err := r.Patch(ctx, hydratedTarget, client.MergeFrom(original)); err != nil {
8✔
189
                        return ctrlResult, errLogAndWrap(log, err, "failed to update HydratedTarget")
×
190
                }
×
191
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update", "Updated HydratedTarget %s/%s", hydratedTarget.Namespace, hydratedTarget.Name)
8✔
192
        }
193

194
        return ctrl.Result{}, nil
51✔
195
}
196

197
// SetupWithManager sets up the controller with the Manager.
198
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
199
        return ctrl.NewControllerManagedBy(mgr).
1✔
200
                For(&solarv1alpha1.Target{}).
1✔
201
                Owns(&solarv1alpha1.HydratedTarget{}).
1✔
202
                Watches(
1✔
203
                        &solarv1alpha1.Profile{},
1✔
204
                        handler.EnqueueRequestsFromMapFunc(r.mapProfileToTargets),
1✔
205
                        builder.WithPredicates(profileSelectionPredicate()),
1✔
206
                ).
1✔
207
                Complete(r)
1✔
208
}
1✔
209

210
// profileSelectionPredicate filters events to only trigger reconciles when the target selector of a profile changes.
211
func profileSelectionPredicate() predicate.Predicate {
5✔
212
        return predicate.Funcs{
5✔
213
                UpdateFunc: func(e event.UpdateEvent) bool {
8✔
214
                        oldObj, ok1 := e.ObjectOld.(*solarv1alpha1.Profile)
3✔
215
                        newObj, ok2 := e.ObjectNew.(*solarv1alpha1.Profile)
3✔
216
                        if !ok1 || !ok2 {
3✔
217
                                return false
×
218
                        }
×
219

220
                        return !apiequality.Semantic.DeepEqual(oldObj.Spec.TargetSelector, newObj.Spec.TargetSelector)
3✔
221
                },
222
        }
223
}
224

225
// mapProfileToTargets maps a Profile to a list of Target reconcile requests.
226
func (r *TargetReconciler) mapProfileToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
9✔
227
        log := ctrl.LoggerFrom(ctx)
9✔
228

9✔
229
        profile, ok := obj.(*solarv1alpha1.Profile)
9✔
230
        if !ok {
9✔
231
                log.Error(nil, "Object is not a Profile", "type", fmt.Sprintf("%T", obj))
×
232
                return nil
×
233
        }
×
234

235
        selector, err := metav1.LabelSelectorAsSelector(&profile.Spec.TargetSelector)
9✔
236
        if err != nil {
9✔
237
                log.Error(err, "Invalid targetSelector in Profile", "profile", profile.Name, "targetSelector", profile.Spec.TargetSelector.String())
×
238
                return nil
×
239
        }
×
240

241
        targetList := &solarv1alpha1.TargetList{}
9✔
242
        err = r.List(ctx, targetList,
9✔
243
                client.InNamespace(profile.GetNamespace()),
9✔
244
                client.MatchingLabelsSelector{Selector: selector},
9✔
245
        )
9✔
246
        if err != nil {
9✔
247
                log.V(1).Error(err, "Failed to list Targets for Profile", "profile", profile.Name)
×
248
                return nil
×
249
        }
×
250

251
        requests := make([]reconcile.Request, 0, len(targetList.Items))
9✔
252
        for _, target := range targetList.Items {
16✔
253
                requests = append(requests, reconcile.Request{
7✔
254
                        NamespacedName: types.NamespacedName{
7✔
255
                                Name:      target.Name,
7✔
256
                                Namespace: target.Namespace,
7✔
257
                        },
7✔
258
                })
7✔
259
        }
7✔
260

261
        return requests
9✔
262
}
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