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

opendefensecloud / solution-arsenal / 23897527920

02 Apr 2026 11:09AM UTC coverage: 71.176% (-0.1%) from 71.272%
23897527920

push

github

web-flow
fix(deps): update github.com/mandelsoft/goutils digest to af3f275 (#354)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

2210 of 3105 relevant lines covered (71.18%)

22.95 hits per line

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

86.54
/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/builder"
22
        "sigs.k8s.io/controller-runtime/pkg/client"
23
        "sigs.k8s.io/controller-runtime/pkg/handler"
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
        // WatchNamespace restricts reconciliation to this namespace.
38
        // Should be empty in production (watches all namespaces).
39
        // Intended for use in integration tests only.
40
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
41
        WatchNamespace string
42
}
43

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

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

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

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

115✔
72
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
126✔
73
                return ctrlResult, nil
11✔
74
        }
11✔
75

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

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

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

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

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

107
                return ctrlResult, nil
1✔
108
        }
109

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

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

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

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

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

5✔
149
                        if apierrors.IsNotFound(err) {
10✔
150
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
5✔
151
                        }
5✔
152

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

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

165
        // RenderTask still running
166
        return ctrlResult, nil
50✔
167
}
168

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

174
        log := ctrl.LoggerFrom(ctx)
50✔
175

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

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

1✔
188
                return changed
1✔
189
        }
1✔
190

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

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

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

1✔
208
                return changed
1✔
209
        }
210

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

48✔
213
        return false
48✔
214
}
215

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

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

226
        spec, err := r.computeRenderTaskSpec(ctx, res)
15✔
227
        if err != nil {
20✔
228
                return err
5✔
229
        }
5✔
230
        rt := &solarv1alpha1.RenderTask{
10✔
231
                ObjectMeta: metav1.ObjectMeta{
10✔
232
                        Name: renderTaskName(res),
10✔
233
                },
10✔
234
                Spec: spec,
10✔
235
        }
10✔
236
        rt.Spec.OwnerName = res.Name
10✔
237
        rt.Spec.OwnerNamespace = res.Namespace
10✔
238
        rt.Spec.OwnerKind = "Release"
10✔
239

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

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

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

256
        return nil
10✔
257
}
258

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

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

271
        return nil
×
272
}
273

274
func (r *ReleaseReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.Release) (solarv1alpha1.RenderTaskSpec, error) {
15✔
275
        spec := solarv1alpha1.RenderTaskSpec{}
15✔
276

15✔
277
        cvRef := types.NamespacedName{
15✔
278
                Name:      res.Spec.ComponentVersionRef.Name,
15✔
279
                Namespace: res.Namespace,
15✔
280
        }
15✔
281

15✔
282
        cv := &solarv1alpha1.ComponentVersion{}
15✔
283
        if err := r.Get(ctx, cvRef, cv); err != nil {
20✔
284
                return spec, err
5✔
285
        }
5✔
286

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

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

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

10✔
316
        return spec, nil
10✔
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
                Watches(&solarv1alpha1.RenderTask{},
1✔
324
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Release")),
1✔
325
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
326
                ).
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