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

opendefensecloud / solution-arsenal / 22091412646

17 Feb 2026 08:36AM UTC coverage: 70.186% (+9.1%) from 61.119%
22091412646

push

github

jastBytes
Adopt events API to new controllers

17 of 21 new or added lines in 3 files covered. (80.95%)

49 existing lines in 4 files now uncovered.

1509 of 2150 relevant lines covered (70.19%)

15.44 hits per line

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

84.76
/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
        "strings"
12
        "time"
13

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

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

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

32
// ReleaseReconciler reconciles a Release object
33
type ReleaseReconciler struct {
34
        client.Client
35
        Scheme      *runtime.Scheme
36
        Recorder    events.EventRecorder
37
        PushOptions solarv1alpha1.PushOptions
38
}
39

40
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch;create;update;patch;delete
41
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/status,verbs=get;update;patch
42
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/finalizers,verbs=update
43
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
44
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
45
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
46

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

61
func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
132✔
62
        log := ctrl.LoggerFrom(ctx)
132✔
63
        ctrlResult := ctrl.Result{}
132✔
64

132✔
65
        log.V(1).Info("Release is being reconciled", "req", req)
132✔
66

132✔
67
        // Fetch the Release instance
132✔
68
        res := &solarv1alpha1.Release{}
132✔
69
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
133✔
70
                if apierrors.IsNotFound(err) {
2✔
71
                        // Object not found, return. Created objects are automatically garbage collected.
1✔
72
                        return ctrlResult, nil
1✔
73
                }
1✔
74

75
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
76
        }
77

78
        // Handle deletion: cleanup rendertask, then remove finalizer
79
        if !res.DeletionTimestamp.IsZero() {
132✔
80
                log.V(1).Info("Release is being deleted")
1✔
81
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Release is being deleted, cleaning up resources")
1✔
82

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

87
                // Remove finalizer
88
                if slices.Contains(res.Finalizers, releaseFinalizer) {
2✔
89
                        log.V(1).Info("Removing finalizer from resource")
1✔
90
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
2✔
91
                                return f == releaseFinalizer
1✔
92
                        })
1✔
93
                        if err := r.Update(ctx, res); err != nil {
1✔
94
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
×
95
                        }
×
96
                }
97

98
                return ctrlResult, nil
1✔
99
        }
100

101
        // Add finalizer if not present and not deleting
102
        if res.DeletionTimestamp.IsZero() {
260✔
103
                if !slices.Contains(res.Finalizers, releaseFinalizer) {
144✔
104
                        log.V(1).Info("Adding finalizer to resource")
14✔
105
                        res.Finalizers = append(res.Finalizers, releaseFinalizer)
14✔
106
                        if err := r.Update(ctx, res); err != nil {
14✔
107
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
108
                        }
×
109
                        // Return without requeue; the Update event will trigger reconciliation again
110
                        return ctrlResult, nil
14✔
111
                }
112
        }
113

114
        // Check if rendertask has already completed successfully
115
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskCompleted)
116✔
116
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
117✔
117
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
1✔
118
                return ctrlResult, nil
1✔
119
        }
1✔
120

121
        // Check if rendertask has already failed
122
        fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskFailed)
115✔
123
        if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue {
116✔
124
                log.V(1).Info("RenderTask has already failed, no further action needed")
1✔
125
                return ctrlResult, nil
1✔
126
        }
1✔
127

128
        // Reconcile RenderTask
129
        rt := &solarv1alpha1.RenderTask{}
114✔
130
        err := r.Get(ctx, client.ObjectKey{Name: generationName(res), Namespace: res.Namespace}, rt)
114✔
131
        if client.IgnoreNotFound(err) != nil {
114✔
132
                log.V(1).Info("Failed to get render task", "res", res)
×
133
                return ctrlResult, errLogAndWrap(log, err, "failed to get RenderTask")
×
134
        }
×
135

136
        if apierrors.IsNotFound(err) {
216✔
137
                if err := r.createRenderTask(ctx, res); err != nil {
196✔
138
                        log.V(1).Info("Failed to create RenderTask", "res", res)
94✔
139
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "failed to create RenderTask")
94✔
140

94✔
141
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, errLogAndWrap(log, err, "failed to create RenderTask")
94✔
142
                }
94✔
143
                log.V(1).Info("Created RenderTask", "res", res)
8✔
144
                r.Recorder.Eventf(res, rt, corev1.EventTypeNormal, "Created", "Create", "RenderTask was created")
8✔
145
        }
146

147
        if changed := r.updateStatusConditionsFromRenderTask(ctx, res, rt); changed {
22✔
148
                if err := r.Status().Update(ctx, res); err != nil {
2✔
149
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
150
                }
×
151
        }
152

153
        // RenderTask still running, requeue
154
        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
20✔
155
}
156

157
func (r *ReleaseReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.Release, rt *solarv1alpha1.RenderTask) (changed bool) {
20✔
158
        if rt == nil || res == nil {
20✔
159
                return false
×
160
        }
×
161

162
        log := ctrl.LoggerFrom(ctx)
20✔
163

20✔
164
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
21✔
165
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
166
                        Type:               ConditionTypeTaskFailed,
1✔
167
                        Status:             metav1.ConditionTrue,
1✔
168
                        ObservedGeneration: res.Generation,
1✔
169
                        Reason:             "TaskFailed",
1✔
170
                        Message:            "RenderTask failed",
1✔
171
                })
1✔
172

1✔
173
                log.V(1).Info("RenderTask failed", "name", rt.Name)
1✔
174
                r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskFailed", "RunTask", "RenderTask failed")
1✔
175

1✔
176
                return changed
1✔
177
        }
1✔
178

179
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) {
20✔
180
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
181
                        Type:               ConditionTypeTaskCompleted,
1✔
182
                        Status:             metav1.ConditionTrue,
1✔
183
                        ObservedGeneration: res.Generation,
1✔
184
                        Reason:             "TaskCompleted",
1✔
185
                        Message:            "RenderTask completed",
1✔
186
                })
1✔
187

1✔
188
                if res.Status.ChartURL != rt.Status.ChartURL {
1✔
189
                        res.Status.ChartURL = rt.Status.ChartURL
×
190
                        changed = true
×
191
                }
×
192

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

1✔
196
                return changed
1✔
197
        }
198

199
        log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name)
18✔
200

18✔
201
        return false
18✔
202
}
203

204
func (r *ReleaseReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.Release) error {
102✔
205
        log := ctrl.LoggerFrom(ctx)
102✔
206

102✔
207
        // Check if we need to cleanup an old task
102✔
208
        if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" {
103✔
209
                if err := r.deleteRenderTask(ctx, res); err != nil {
1✔
210
                        return errLogAndWrap(log, err, "failed to cleanup old task")
×
211
                }
×
212
        }
213

214
        cfg, err := r.computeRendererConfig(ctx, res)
102✔
215
        if err != nil {
195✔
216
                return err
93✔
217
        }
93✔
218
        if cfg == nil {
9✔
219
                return fmt.Errorf("unexpected nil RendererConfig")
×
220
        }
×
221

222
        rt := &solarv1alpha1.RenderTask{
9✔
223
                ObjectMeta: metav1.ObjectMeta{
9✔
224
                        Name:      generationName(res),
9✔
225
                        Namespace: res.Namespace,
9✔
226
                },
9✔
227
                Spec: solarv1alpha1.RenderTaskSpec{
9✔
228
                        RendererConfig: *cfg,
9✔
229
                },
9✔
230
        }
9✔
231

9✔
232
        if err := r.Create(ctx, rt); err != nil {
9✔
NEW
233
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err)
×
234
                return errLogAndWrap(log, err, "failed to create RenderTask")
×
235
        }
×
236

237
        // Set owner references
238
        if err := controllerutil.SetControllerReference(res, rt, r.Scheme); err != nil {
9✔
239
                return errLogAndWrap(log, err, "failed to set controller reference")
×
240
        }
×
241

242
        // Set Reference in Status
243
        res.Status.RenderTaskRef = &corev1.ObjectReference{
9✔
244
                APIVersion: solarv1alpha1.SchemeGroupVersion.String(),
9✔
245
                Kind:       "RenderTask",
9✔
246
                Namespace:  rt.Namespace,
9✔
247
                Name:       rt.Name,
9✔
248
        }
9✔
249

9✔
250
        if err := r.Status().Update(ctx, res); err != nil {
10✔
251
                return errLogAndWrap(log, err, "failed to update status")
1✔
252
        }
1✔
253

254
        return nil
8✔
255
}
256

257
func (r *ReleaseReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.Release) error {
2✔
258
        if res.Status.RenderTaskRef == nil {
2✔
259
                return nil
×
260
        }
×
261

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

269
        return nil
×
270
}
271

272
func (r *ReleaseReconciler) computeRendererConfig(ctx context.Context, res *solarv1alpha1.Release) (*solarv1alpha1.RendererConfig, error) {
102✔
273
        cvRef := types.NamespacedName{
102✔
274
                Name:      res.Spec.ComponentVersionRef.Name,
102✔
275
                Namespace: res.Namespace,
102✔
276
        }
102✔
277

102✔
278
        cv := &solarv1alpha1.ComponentVersion{}
102✔
279
        if err := r.Get(ctx, cvRef, cv); err != nil {
195✔
280
                return nil, err
93✔
281
        }
93✔
282

283
        po := r.PushOptions
9✔
284
        url, err := url.JoinPath(po.ReferenceURL, res.Namespace, fmt.Sprintf("ht-%s", res.Name))
9✔
285
        if err != nil {
9✔
286
                return nil, err
×
287
        }
×
288

289
        if !strings.HasPrefix(url, "oci://") {
18✔
290
                url = fmt.Sprintf("oci://%s", url)
9✔
291
        }
9✔
292

293
        version := fmt.Sprintf("v0.0.%d", res.GetGeneration())
9✔
294
        url = fmt.Sprintf("%s:%s", url, version)
9✔
295

9✔
296
        po.ReferenceURL = url
9✔
297

9✔
298
        return &solarv1alpha1.RendererConfig{
9✔
299
                Type: solarv1alpha1.RendererConfigTypeRelease,
9✔
300
                ReleaseConfig: solarv1alpha1.ReleaseConfig{
9✔
301
                        Chart: solarv1alpha1.ChartConfig{
9✔
302
                                Name:        res.Name,
9✔
303
                                Description: fmt.Sprintf("Release of %s", res.Spec.ComponentVersionRef.Name),
9✔
304
                                Version:     version,
9✔
305
                                AppVersion:  version,
9✔
306
                        },
9✔
307
                        Input: solarv1alpha1.ReleaseInput{
9✔
308
                                Component: solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
9✔
309
                                Helm:      cv.Spec.Helm,
9✔
310
                                KRO:       cv.Spec.KRO,
9✔
311
                                Resources: cv.Spec.Resources,
9✔
312
                        },
9✔
313
                        Values: res.Spec.Values,
9✔
314
                },
9✔
315
                PushOptions: po,
9✔
316
        }, nil
9✔
317
}
318

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