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

opendefensecloud / solution-arsenal / 23740992731

30 Mar 2026 10:50AM UTC coverage: 71.908% (-0.2%) from 72.141%
23740992731

Pull #341

github

web-flow
Merge 01d0ae5dc into 0fe88d665
Pull Request #341: fix: ensure getHandler in discovery returns handler on subsequent calls

10 of 10 new or added lines in 1 file covered. (100.0%)

9 existing lines in 3 files now uncovered.

2209 of 3072 relevant lines covered (71.91%)

22.33 hits per line

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

85.99
/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
        "fmt"
9
        "net/url"
10
        "slices"
11
        "time"
12

13
        corev1 "k8s.io/api/core/v1"
14
        apierrors "k8s.io/apimachinery/pkg/api/errors"
15
        apimeta "k8s.io/apimachinery/pkg/api/meta"
16
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17
        "k8s.io/apimachinery/pkg/runtime"
18
        "k8s.io/apimachinery/pkg/types"
19
        "k8s.io/client-go/tools/events"
20
        ctrl "sigs.k8s.io/controller-runtime"
21
        "sigs.k8s.io/controller-runtime/pkg/client"
22
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
23

24
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
25
)
26

27
const (
28
        releaseFinalizer = "solar.opendefense.cloud/release-finalizer"
29
)
30

31
// ReleaseReconciler reconciles a Release object
32
type ReleaseReconciler struct {
33
        client.Client
34
        Scheme   *runtime.Scheme
35
        Recorder events.EventRecorder
36
        // WatchNamespace restricts reconciliation to this namespace.
37
        // Should be empty in production (watches all namespaces).
38
        // Intended for use in integration tests only.
39
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
40
        WatchNamespace string
41
}
42

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

51
// Reconcile moves the current state of the cluster closer to the desired state
52
//
53
// Reconciliation Flow:
54
//
55
//        Release created
56
//            ↓
57
//        Add finalizer
58
//            ↓
59
//        Check if already succeeded → YES → Return (no-op)
60
//            ↓ NO
61
//        Get or create RenderTask
62
//            ↓
63
//        Update status from RenderTask
64

65
func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
51✔
66
        log := ctrl.LoggerFrom(ctx)
51✔
67
        ctrlResult := ctrl.Result{}
51✔
68

51✔
69
        log.V(1).Info("Release is being reconciled", "req", req)
51✔
70

51✔
71
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
56✔
72
                return ctrlResult, nil
5✔
73
        }
5✔
74

75
        // Fetch the Release instance
76
        res := &solarv1alpha1.Release{}
46✔
77
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
47✔
78
                if apierrors.IsNotFound(err) {
2✔
79
                        // Object not found, return. Created objects are automatically garbage collected.
1✔
80
                        return ctrlResult, nil
1✔
81
                }
1✔
82

83
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
84
        }
85

86
        // Handle deletion: cleanup rendertask, then remove finalizer
87
        if !res.DeletionTimestamp.IsZero() {
46✔
88
                log.V(1).Info("Release is being deleted")
1✔
89
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Release is being deleted, cleaning up resources")
1✔
90

1✔
91
                if err := r.deleteRenderTask(ctx, res); client.IgnoreNotFound(err) != nil {
1✔
92
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete render task")
×
93
                }
×
94

95
                // Remove finalizer
96
                if slices.Contains(res.Finalizers, releaseFinalizer) {
2✔
97
                        log.V(1).Info("Removing finalizer from resource")
1✔
98
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
2✔
99
                                return f == releaseFinalizer
1✔
100
                        })
1✔
101
                        if err := r.Update(ctx, res); err != nil {
1✔
102
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
×
103
                        }
×
104
                }
105

106
                return ctrlResult, nil
1✔
107
        }
108

109
        // Add finalizer if not present and not deleting
110
        if res.DeletionTimestamp.IsZero() {
88✔
111
                if !slices.Contains(res.Finalizers, releaseFinalizer) {
59✔
112
                        log.V(1).Info("Adding finalizer to resource")
15✔
113
                        res.Finalizers = append(res.Finalizers, releaseFinalizer)
15✔
114
                        if err := r.Update(ctx, res); err != nil {
15✔
115
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
116
                        }
×
117
                        // Return without requeue; the Update event will trigger reconciliation again
118
                        return ctrlResult, nil
15✔
119
                }
120
        }
121

122
        // Check if rendertask has already completed successfully
123
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskCompleted)
29✔
124
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
30✔
125
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
1✔
126
                return ctrlResult, nil
1✔
127
        }
1✔
128

129
        // Check if rendertask has already failed
130
        fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskFailed)
28✔
131
        if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue {
29✔
132
                log.V(1).Info("RenderTask has already failed, no further action needed")
1✔
133
                return ctrlResult, nil
1✔
134
        }
1✔
135

136
        // Reconcile RenderTask
137
        rt := &solarv1alpha1.RenderTask{}
27✔
138
        err := r.Get(ctx, client.ObjectKey{Name: generationName(res), Namespace: res.Namespace}, rt)
27✔
139
        if client.IgnoreNotFound(err) != nil {
27✔
140
                return ctrlResult, errLogAndWrap(log, err, "failed to get RenderTask")
×
141
        }
×
142

143
        if apierrors.IsNotFound(err) {
44✔
144
                if err := r.createRenderTask(ctx, res); err != nil {
25✔
145
                        log.V(1).Error(err, "Failed to create RenderTask")
8✔
146
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", fmt.Sprintf("failed to create RenderTask: %q", err))
8✔
147

8✔
148
                        if apierrors.IsNotFound(err) {
15✔
149
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
7✔
150
                        }
7✔
151

152
                        return ctrlResult, errLogAndWrap(log, err, "failed to create RenderTask")
1✔
153
                }
154
                log.V(1).Info("Created RenderTask", "res", res)
9✔
155
                r.Recorder.Eventf(res, rt, corev1.EventTypeNormal, "Created", "Create", "RenderTask was created")
9✔
156
        }
157

158
        if changed := r.updateStatusConditionsFromRenderTask(ctx, res, rt); changed {
21✔
159
                if err := r.Status().Update(ctx, res); err != nil {
2✔
160
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
161
                }
×
162
        }
163

164
        // RenderTask still running, requeue
165
        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
19✔
166
}
167

168
func (r *ReleaseReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.Release, rt *solarv1alpha1.RenderTask) (changed bool) {
19✔
169
        if rt == nil || res == nil {
19✔
170
                return false
×
171
        }
×
172

173
        log := ctrl.LoggerFrom(ctx)
19✔
174

19✔
175
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
20✔
176
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
177
                        Type:               ConditionTypeTaskFailed,
1✔
178
                        Status:             metav1.ConditionTrue,
1✔
179
                        ObservedGeneration: res.Generation,
1✔
180
                        Reason:             "TaskFailed",
1✔
181
                        Message:            "RenderTask failed",
1✔
182
                })
1✔
183

1✔
184
                log.V(1).Info("RenderTask failed", "name", rt.Name)
1✔
185
                r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskFailed", "RunTask", "RenderTask failed")
1✔
186

1✔
187
                return changed
1✔
188
        }
1✔
189

190
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) {
19✔
191
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
192
                        Type:               ConditionTypeTaskCompleted,
1✔
193
                        Status:             metav1.ConditionTrue,
1✔
194
                        ObservedGeneration: res.Generation,
1✔
195
                        Reason:             "TaskCompleted",
1✔
196
                        Message:            "RenderTask completed",
1✔
197
                })
1✔
198

1✔
199
                if res.Status.ChartURL != rt.Status.ChartURL {
1✔
200
                        res.Status.ChartURL = rt.Status.ChartURL
×
201
                        changed = true
×
202
                }
×
203

204
                log.V(1).Info("RenderTask succeeded", "name", rt.Name)
1✔
205
                r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskCompleted", "RunTask", "RenderTask completed successfully")
1✔
206

1✔
207
                return changed
1✔
208
        }
209

210
        log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name)
17✔
211

17✔
212
        return false
17✔
213
}
214

215
func (r *ReleaseReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.Release) error {
17✔
216
        log := ctrl.LoggerFrom(ctx)
17✔
217

17✔
218
        // Check if we need to cleanup an old task
17✔
219
        if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" {
18✔
220
                if err := r.deleteRenderTask(ctx, res); err != nil {
1✔
221
                        return errLogAndWrap(log, err, "failed to cleanup old task")
×
222
                }
×
223
        }
224

225
        spec, err := r.computeRenderTaskSpec(ctx, res)
17✔
226
        if err != nil {
24✔
227
                return err
7✔
228
        }
7✔
229
        rt := &solarv1alpha1.RenderTask{
10✔
230
                ObjectMeta: metav1.ObjectMeta{
10✔
231
                        Name:      generationName(res),
10✔
232
                        Namespace: res.Namespace,
10✔
233
                },
10✔
234
                Spec: spec,
10✔
235
        }
10✔
236

10✔
237
        if err := r.Create(ctx, rt); err != nil {
10✔
UNCOV
238
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err)
×
UNCOV
239
                return errLogAndWrap(log, err, "failed to create RenderTask")
×
UNCOV
240
        }
×
241

242
        // Set owner references
243
        if err := controllerutil.SetControllerReference(res, rt, r.Scheme); err != nil {
10✔
244
                return errLogAndWrap(log, err, "failed to set controller reference")
×
245
        }
×
246

247
        // Set Reference in Status
248
        res.Status.RenderTaskRef = &corev1.ObjectReference{
10✔
249
                APIVersion: solarv1alpha1.SchemeGroupVersion.String(),
10✔
250
                Kind:       "RenderTask",
10✔
251
                Namespace:  rt.Namespace,
10✔
252
                Name:       rt.Name,
10✔
253
        }
10✔
254

10✔
255
        if err := r.Status().Update(ctx, res); err != nil {
11✔
256
                return errLogAndWrap(log, err, "failed to update status")
1✔
257
        }
1✔
258

259
        return nil
9✔
260
}
261

262
func (r *ReleaseReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.Release) error {
2✔
263
        if res.Status.RenderTaskRef == nil {
2✔
264
                return nil
×
265
        }
×
266

267
        rt := &solarv1alpha1.RenderTask{}
2✔
268
        if err := r.Get(ctx, client.ObjectKey{Name: res.Status.RenderTaskRef.Name, Namespace: res.Status.RenderTaskRef.Namespace}, rt); client.IgnoreNotFound(err) != nil {
2✔
269
                return err
×
270
        } else if err == nil {
4✔
271
                return r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground))
2✔
272
        }
2✔
273

274
        return nil
×
275
}
276

277
func (r *ReleaseReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.Release) (solarv1alpha1.RenderTaskSpec, error) {
17✔
278
        spec := solarv1alpha1.RenderTaskSpec{}
17✔
279

17✔
280
        cvRef := types.NamespacedName{
17✔
281
                Name:      res.Spec.ComponentVersionRef.Name,
17✔
282
                Namespace: res.Namespace,
17✔
283
        }
17✔
284

17✔
285
        cv := &solarv1alpha1.ComponentVersion{}
17✔
286
        if err := r.Get(ctx, cvRef, cv); err != nil {
24✔
287
                return spec, err
7✔
288
        }
7✔
289

290
        chartName := fmt.Sprintf("release-%s", res.Name)
10✔
291
        repo, err := url.JoinPath(res.Namespace, chartName)
10✔
292
        if err != nil {
10✔
293
                return spec, err
×
294
        }
×
295

296
        tag := fmt.Sprintf("v0.0.%d", res.GetGeneration())
10✔
297

10✔
298
        spec.RendererConfig = solarv1alpha1.RendererConfig{
10✔
299
                Type: solarv1alpha1.RendererConfigTypeRelease,
10✔
300
                ReleaseConfig: solarv1alpha1.ReleaseConfig{
10✔
301
                        Chart: solarv1alpha1.ChartConfig{
10✔
302
                                Name:        chartName,
10✔
303
                                Description: fmt.Sprintf("Release of %s", res.Spec.ComponentVersionRef.Name),
10✔
304
                                Version:     tag,
10✔
305
                                AppVersion:  tag,
10✔
306
                        },
10✔
307
                        Input: solarv1alpha1.ReleaseInput{
10✔
308
                                Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
10✔
309
                                Resources:  cv.Spec.Resources,
10✔
310
                                Entrypoint: cv.Spec.Entrypoint,
10✔
311
                        },
10✔
312
                        Values: res.Spec.Values,
10✔
313
                },
10✔
314
        }
10✔
315
        spec.Repository = repo
10✔
316
        spec.Tag = tag
10✔
317
        spec.FailedJobTTL = res.Spec.FailedJobTTL
10✔
318

10✔
319
        return spec, nil
10✔
320
}
321

322
// SetupWithManager sets up the controller with the Manager.
323
func (r *ReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
324
        return ctrl.NewControllerManagedBy(mgr).
1✔
325
                For(&solarv1alpha1.Release{}).
1✔
326
                Owns(&solarv1alpha1.RenderTask{}).
1✔
327
                Complete(r)
1✔
328
}
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