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

opendefensecloud / solution-arsenal / 23764791425

30 Mar 2026 07:57PM UTC coverage: 72.056% (-0.09%) from 72.141%
23764791425

Pull #338

github

web-flow
Merge 4bff943de into 0fe88d665
Pull Request #338: feat: reworked rendertask to be a clusterwide ressource, modified tes…

81 of 94 new or added lines in 5 files covered. (86.17%)

20 existing lines in 5 files now uncovered.

2215 of 3074 relevant lines covered (72.06%)

29.01 hits per line

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

87.04
/pkg/controller/hydratedtarget_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
        ociname "github.com/google/go-containerregistry/pkg/name"
15
        corev1 "k8s.io/api/core/v1"
16
        apierrors "k8s.io/apimachinery/pkg/api/errors"
17
        apimeta "k8s.io/apimachinery/pkg/api/meta"
18
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19
        "k8s.io/apimachinery/pkg/runtime"
20
        "k8s.io/client-go/tools/events"
21
        ctrl "sigs.k8s.io/controller-runtime"
22
        "sigs.k8s.io/controller-runtime/pkg/builder"
23
        "sigs.k8s.io/controller-runtime/pkg/client"
24
        "sigs.k8s.io/controller-runtime/pkg/handler"
25

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

29
const (
30
        hydratedTargetFinalizer = "solar.opendefense.cloud/hydrated-target-finalizer"
31
)
32

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

45
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets,verbs=get;list;watch;create;update;patch;delete
46
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets/status,verbs=get;update;patch
47
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets/finalizers,verbs=update
48
// FIXME: Switch out releases for profiles                      👇
49
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
50
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
51
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
52
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
53

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

68
func (r *HydratedTargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
157✔
69
        log := ctrl.LoggerFrom(ctx)
157✔
70
        ctrlResult := ctrl.Result{}
157✔
71

157✔
72
        log.V(1).Info("HydratedTarget is being reconciled", "req", req)
157✔
73

157✔
74
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
178✔
75
                return ctrlResult, nil
21✔
76
        }
21✔
77

78
        // Fetch the HydratedTarget instance
79
        res := &solarv1alpha1.HydratedTarget{}
136✔
80
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
139✔
81
                if apierrors.IsNotFound(err) {
6✔
82
                        // Object not found, return. Created objects are automatically garbage collected.
3✔
83
                        return ctrlResult, nil
3✔
84
                }
3✔
85

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

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

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

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

109
                return ctrlResult, nil
2✔
110
        }
111

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

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

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

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

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

5✔
151
                        if apierrors.IsNotFound(err) {
9✔
152
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
4✔
153
                        }
4✔
154

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

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

167
        // RenderTask still running, requeue
168
        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
105✔
169
}
170

171
func (r *HydratedTargetReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget, rt *solarv1alpha1.RenderTask) (changed bool) {
105✔
172
        if rt == nil || res == nil {
105✔
173
                return false
×
174
        }
×
175

176
        log := ctrl.LoggerFrom(ctx)
105✔
177

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

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

1✔
190
                return changed
1✔
191
        }
1✔
192

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

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

1✔
205
                return changed
1✔
206
        }
1✔
207

208
        log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name)
103✔
209

103✔
210
        return false
103✔
211
}
212

213
func (r *HydratedTargetReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget) error {
27✔
214
        log := ctrl.LoggerFrom(ctx)
27✔
215

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

223
        spec, err := r.computeRenderTaskSpec(ctx, res)
27✔
224
        if err != nil {
31✔
225
                return err
4✔
226
        }
4✔
227
        rt := &solarv1alpha1.RenderTask{
23✔
228
                ObjectMeta: metav1.ObjectMeta{
23✔
229
                        Name:   renderTaskName(res),
23✔
230
                        Labels: renderTaskLabels(res, "HydratedTarget"),
23✔
231
                },
23✔
232
                Spec: spec,
23✔
233
        }
23✔
234

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

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

23✔
247
        if err := r.Status().Update(ctx, res); err != nil {
24✔
248
                return errLogAndWrap(log, err, "failed to update status")
1✔
249
        }
1✔
250

251
        return nil
22✔
252
}
253

254
func (r *HydratedTargetReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget) error {
8✔
255
        if res.Status.RenderTaskRef == nil {
9✔
256
                return nil
1✔
257
        }
1✔
258

259
        rt := &solarv1alpha1.RenderTask{}
7✔
260
        if err := r.Get(ctx, client.ObjectKey{Name: res.Status.RenderTaskRef.Name}, rt); client.IgnoreNotFound(err) != nil {
7✔
261
                return err
×
262
        } else if err == nil {
14✔
263
                return r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground))
7✔
264
        }
7✔
265

UNCOV
266
        return nil
×
267
}
268

269
func (r *HydratedTargetReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.HydratedTarget) (solarv1alpha1.RenderTaskSpec, error) {
27✔
270
        spec := solarv1alpha1.RenderTaskSpec{}
27✔
271

27✔
272
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
27✔
273
        for k, v := range res.Spec.Releases {
40✔
274
                rel := &solarv1alpha1.Release{}
13✔
275
                if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, rel); err != nil {
17✔
276
                        return spec, err
4✔
277
                }
4✔
278

279
                if rel.Status.ChartURL == "" {
9✔
280
                        return spec, fmt.Errorf("Release reference was empty, check if the release chart was rendered correctly.")
×
281
                }
×
282

283
                ref, err := ociname.ParseReference(rel.Status.ChartURL)
9✔
284
                if err != nil {
9✔
285
                        return spec, err
×
286
                }
×
287

288
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
9✔
289
                if err != nil {
9✔
290
                        return spec, err
×
291
                }
×
292

293
                resolvedReleases[k] = solarv1alpha1.ResourceAccess{
9✔
294
                        Repository: strings.TrimPrefix(repo, "oci://"),
9✔
295
                        Tag:        ref.Identifier(),
9✔
296
                }
9✔
297
        }
298

299
        resolvedReleaseNames := []string{}
23✔
300
        for k := range resolvedReleases {
32✔
301
                resolvedReleaseNames = append(resolvedReleaseNames, k)
9✔
302
        }
9✔
303

304
        chartName := fmt.Sprintf("ht-%s", res.Name)
23✔
305
        repo, err := url.JoinPath(res.Namespace, chartName)
23✔
306
        if err != nil {
23✔
307
                return spec, err
×
308
        }
×
309

310
        tag := fmt.Sprintf("v0.0.%d", res.GetGeneration())
23✔
311

23✔
312
        spec.RendererConfig = solarv1alpha1.RendererConfig{
23✔
313
                Type: solarv1alpha1.RendererConfigTypeHydratedTarget,
23✔
314
                HydratedTargetConfig: solarv1alpha1.HydratedTargetConfig{
23✔
315
                        Chart: solarv1alpha1.ChartConfig{
23✔
316
                                Name:        chartName,
23✔
317
                                Description: fmt.Sprintf("HydratedTarget of %v", resolvedReleaseNames),
23✔
318
                                Version:     tag,
23✔
319
                                AppVersion:  tag,
23✔
320
                        },
23✔
321
                        Input: solarv1alpha1.HydratedTargetInput{
23✔
322
                                Releases: resolvedReleases,
23✔
323
                                Userdata: res.Spec.Userdata,
23✔
324
                        },
23✔
325
                },
23✔
326
        }
23✔
327
        spec.Repository = repo
23✔
328
        spec.Tag = tag
23✔
329

23✔
330
        return spec, nil
23✔
331
}
332

333
// SetupWithManager sets up the controller with the Manager.
334
func (r *HydratedTargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
335
        return ctrl.NewControllerManagedBy(mgr).
1✔
336
                For(&solarv1alpha1.HydratedTarget{}).
1✔
337
                Watches(&solarv1alpha1.RenderTask{},
1✔
338
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("HydratedTarget")),
1✔
339
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
340
                ).
1✔
341
                Complete(r)
1✔
342
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc