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

opendefensecloud / solution-arsenal / 23786905907

31 Mar 2026 08:01AM UTC coverage: 71.873% (+0.3%) from 71.547%
23786905907

push

github

web-flow
fix: ensure getHandler in discovery returns handler on subsequent calls (#341)

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

6 existing lines in 3 files now uncovered.

2172 of 3022 relevant lines covered (71.87%)

16.08 hits per line

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

87.56
/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/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
        hydratedTargetFinalizer = "solar.opendefense.cloud/hydrated-target-finalizer"
30
)
31

32
// HydratedTargetReconciler reconciles a HydratedTarget object
33
type HydratedTargetReconciler 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=hydratedtargets,verbs=get;list;watch;create;update;patch;delete
45
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets/status,verbs=get;update;patch
46
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets/finalizers,verbs=update
47
// FIXME: Switch out releases for profiles                      👇
48
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
49
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
50
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
51
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
52

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

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

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

91✔
73
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
106✔
74
                return ctrlResult, nil
15✔
75
        }
15✔
76

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

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

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

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

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

108
                return ctrlResult, nil
2✔
109
        }
110

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

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

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

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

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

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

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

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

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

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

175
        log := ctrl.LoggerFrom(ctx)
46✔
176

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

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

1✔
189
                return changed
1✔
190
        }
1✔
191

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

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

1✔
204
                return changed
1✔
205
        }
1✔
206

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

44✔
209
        return false
44✔
210
}
211

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

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

222
        spec, err := r.computeRenderTaskSpec(ctx, res)
29✔
223
        if err != nil {
33✔
224
                return err
4✔
225
        }
4✔
226
        rt := &solarv1alpha1.RenderTask{
25✔
227
                ObjectMeta: metav1.ObjectMeta{
25✔
228
                        Name:      generationName(res),
25✔
229
                        Namespace: res.Namespace,
25✔
230
                },
25✔
231
                Spec: spec,
25✔
232
        }
25✔
233

25✔
234
        if err := r.Create(ctx, rt); err != nil {
27✔
235
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err)
2✔
236
                return errLogAndWrap(log, err, "secret creation failed")
2✔
237
        }
2✔
238

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

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

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

256
        return nil
22✔
257
}
258

259
func (r *HydratedTargetReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget) error {
10✔
260
        if res.Status.RenderTaskRef == nil {
11✔
261
                return nil
1✔
262
        }
1✔
263

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

UNCOV
271
        return nil
×
272
}
273

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

29✔
277
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
29✔
278
        for k, v := range res.Spec.Releases {
42✔
279
                rel := &solarv1alpha1.Release{}
13✔
280
                if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, rel); err != nil {
17✔
281
                        return spec, err
4✔
282
                }
4✔
283

284
                if rel.Status.ChartURL == "" {
9✔
285
                        return spec, fmt.Errorf("Release reference was empty, check if the release chart was rendered correctly.")
×
286
                }
×
287

288
                ref, err := ociname.ParseReference(rel.Status.ChartURL)
9✔
289
                if err != nil {
9✔
290
                        return spec, err
×
291
                }
×
292

293
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
9✔
294
                if err != nil {
9✔
295
                        return spec, err
×
296
                }
×
297

298
                resolvedReleases[k] = solarv1alpha1.ResourceAccess{
9✔
299
                        Repository: strings.TrimPrefix(repo, "oci://"),
9✔
300
                        Tag:        ref.Identifier(),
9✔
301
                }
9✔
302
        }
303

304
        resolvedReleaseNames := []string{}
25✔
305
        for k := range resolvedReleases {
34✔
306
                resolvedReleaseNames = append(resolvedReleaseNames, k)
9✔
307
        }
9✔
308

309
        chartName := fmt.Sprintf("ht-%s", res.Name)
25✔
310
        repo, err := url.JoinPath(res.Namespace, chartName)
25✔
311
        if err != nil {
25✔
312
                return spec, err
×
313
        }
×
314

315
        tag := fmt.Sprintf("v0.0.%d", res.GetGeneration())
25✔
316

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

25✔
335
        return spec, nil
25✔
336
}
337

338
// SetupWithManager sets up the controller with the Manager.
339
func (r *HydratedTargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
340
        return ctrl.NewControllerManagedBy(mgr).
1✔
341
                For(&solarv1alpha1.HydratedTarget{}).
1✔
342
                Owns(&solarv1alpha1.RenderTask{}).
1✔
343
                Complete(r)
1✔
344
}
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