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

opendefensecloud / solution-arsenal / 24346597267

13 Apr 2026 01:41PM UTC coverage: 74.334% (+0.7%) from 73.591%
24346597267

push

github

web-flow
feat: refactor solar-discovery into standalone service (#392)

Closes #382.
Closes #383 
Closes #384 
Closes #385 



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced standalone SolAr Discovery service for registry scanning
and OCM catalog population; Helm chart and values for flexible
deployment added.
  * Discovery config now supports environment-variable substitution.

* **Documentation**
* Added user guide and architecture docs for SolAr Discovery with
examples and deployment guidance.

* **Removed**
* Discovery custom resource and in-cluster discovery controller/worker
APIs removed.

* **Chores**
* Release/build artifacts and image names updated to align with the new
standalone component.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

12 of 22 new or added lines in 1 file covered. (54.55%)

16 existing lines in 4 files now uncovered.

2010 of 2704 relevant lines covered (74.33%)

23.09 hits per line

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

83.07
/pkg/controller/bootstrap_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
        "errors"
9
        "fmt"
10
        "net/url"
11
        "slices"
12
        "strings"
13
        "time"
14

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

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

30
const (
31
        bootstrapFinalizer = "solar.opendefense.cloud/bootstrap-finalizer"
32
)
33

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

46
var ErrReleaseNotRenderedYet = errors.New("release is not rendered yet")
47

48
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps,verbs=get;list;watch;create;update;patch;delete
49
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps/status,verbs=get;update;patch
50
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=bootstraps/finalizers,verbs=update
51
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=profiles,verbs=get;list;watch
52
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
53
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
54
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
55
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
56

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

71
func (r *BootstrapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
123✔
72
        log := ctrl.LoggerFrom(ctx)
123✔
73
        ctrlResult := ctrl.Result{}
123✔
74

123✔
75
        log.V(1).Info("Bootstrap is being reconciled", "req", req)
123✔
76

123✔
77
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
137✔
78
                return ctrlResult, nil
14✔
79
        }
14✔
80

81
        // Fetch the Bootstrap instance
82
        res := &solarv1alpha1.Bootstrap{}
109✔
83
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
111✔
84
                if apierrors.IsNotFound(err) {
4✔
85
                        // Object not found, return. Created objects are automatically garbage collected.
2✔
86
                        return ctrlResult, nil
2✔
87
                }
2✔
88

89
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
90
        }
91

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

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

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

112
                return ctrlResult, nil
2✔
113
        }
114

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

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

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

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

149
        if apierrors.IsNotFound(err) {
117✔
150
                if err := r.createRenderTask(ctx, res); err != nil {
48✔
151
                        if errors.Is(err, ErrReleaseNotRenderedYet) {
16✔
152
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
153
                        }
×
154
                        log.V(1).Error(err, "Failed to create RenderTask")
16✔
155
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", fmt.Sprintf("failed to create RenderTask: %q", err))
16✔
156

16✔
157
                        if apierrors.IsNotFound(err) {
32✔
158
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
16✔
159
                        }
16✔
160

UNCOV
161
                        return ctrlResult, errLogAndWrap(log, err, "failed to create RenderTask")
×
162
                }
163
                log.V(1).Info("Created RenderTask", "res", res)
16✔
164
                r.Recorder.Eventf(res, rt, corev1.EventTypeNormal, "Created", "Create", "RenderTask was created")
16✔
165
        }
166

167
        if changed := r.updateStatusConditionsFromRenderTask(ctx, res, rt); changed {
71✔
168
                if err := r.Status().Update(ctx, res); err != nil {
2✔
169
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
170
                }
×
171
        }
172

173
        // RenderTask still running
174
        return ctrlResult, nil
69✔
175
}
176

177
func (r *BootstrapReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.Bootstrap, rt *solarv1alpha1.RenderTask) (changed bool) {
69✔
178
        if rt == nil || res == nil {
69✔
179
                return false
×
180
        }
×
181

182
        log := ctrl.LoggerFrom(ctx)
69✔
183

69✔
184
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
70✔
185
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
186
                        Type:               ConditionTypeTaskFailed,
1✔
187
                        Status:             metav1.ConditionTrue,
1✔
188
                        ObservedGeneration: res.Generation,
1✔
189
                        Reason:             "TaskFailed",
1✔
190
                        Message:            "RenderTask failed",
1✔
191
                })
1✔
192

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

1✔
196
                return changed
1✔
197
        }
1✔
198

199
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) {
69✔
200
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
201
                        Type:               ConditionTypeTaskCompleted,
1✔
202
                        Status:             metav1.ConditionTrue,
1✔
203
                        ObservedGeneration: res.Generation,
1✔
204
                        Reason:             "TaskCompleted",
1✔
205
                        Message:            "RenderTask completed",
1✔
206
                })
1✔
207

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

1✔
211
                return changed
1✔
212
        }
1✔
213

214
        log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name)
67✔
215

67✔
216
        return false
67✔
217
}
218

219
func (r *BootstrapReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.Bootstrap) error {
32✔
220
        log := ctrl.LoggerFrom(ctx)
32✔
221

32✔
222
        // Check if we need to cleanup an old task
32✔
223
        if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" {
42✔
224
                if err := r.deleteRenderTask(ctx, res); err != nil {
10✔
225
                        return errLogAndWrap(log, err, "failed to cleanup old task")
×
226
                }
×
227
        }
228

229
        spec, err := r.computeRenderTaskSpec(ctx, res)
32✔
230
        if err != nil {
48✔
231
                return err
16✔
232
        }
16✔
233
        rt := &solarv1alpha1.RenderTask{
16✔
234
                ObjectMeta: metav1.ObjectMeta{
16✔
235
                        Name: renderTaskName(res),
16✔
236
                },
16✔
237
                Spec: spec,
16✔
238
        }
16✔
239
        rt.Spec.OwnerName = res.Name
16✔
240
        rt.Spec.OwnerNamespace = res.Namespace
16✔
241
        rt.Spec.OwnerKind = "Bootstrap"
16✔
242

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

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

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

259
        return nil
16✔
260
}
261

262
func (r *BootstrapReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.Bootstrap) error {
12✔
263
        if res.Status.RenderTaskRef == nil {
13✔
264
                return nil
1✔
265
        }
1✔
266

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

274
        return nil
4✔
275
}
276

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

32✔
280
        releases := map[string]*solarv1alpha1.Release{}
32✔
281
        for _, v := range res.Spec.Releases {
45✔
282
                rel := &solarv1alpha1.Release{}
13✔
283
                if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, rel); err != nil {
17✔
284
                        return spec, err
4✔
285
                }
4✔
286
                releases[rel.Name] = rel
9✔
287
        }
288
        for _, v := range res.Spec.Profiles {
58✔
289
                prf := &solarv1alpha1.Profile{}
30✔
290
                if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, prf); err != nil {
30✔
291
                        return spec, err
×
292
                }
×
293
                if _, exists := releases[prf.Spec.ReleaseRef.Name]; exists {
39✔
294
                        continue
9✔
295
                }
296
                rel := &solarv1alpha1.Release{}
21✔
297
                if err := r.Get(ctx, client.ObjectKey{Name: prf.Spec.ReleaseRef.Name, Namespace: res.Namespace}, rel); err != nil {
33✔
298
                        return spec, err
12✔
299
                }
12✔
300
                releases[rel.Name] = rel
9✔
301
        }
302

303
        isReleaseRendered := func(rel *solarv1alpha1.Release) (bool, error) {
34✔
304
                condFailed := apimeta.FindStatusCondition(rel.Status.Conditions, ConditionTypeTaskFailed)
18✔
305
                if condFailed != nil &&
18✔
306
                        condFailed.Status == metav1.ConditionTrue &&
18✔
307
                        condFailed.ObservedGeneration >= rel.Generation {
18✔
308
                        return false, fmt.Errorf("rendering release %s has failed", rel.Name)
×
309
                }
×
310

311
                condCompleted := apimeta.FindStatusCondition(rel.Status.Conditions, ConditionTypeTaskCompleted)
18✔
312

18✔
313
                return condCompleted != nil &&
18✔
314
                        condCompleted.Status == metav1.ConditionTrue &&
18✔
315
                        condCompleted.ObservedGeneration >= rel.Generation, nil
18✔
316
        }
317

318
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
16✔
319
        for k, v := range releases {
34✔
320

18✔
321
                if ok, err := isReleaseRendered(v); !ok {
18✔
322
                        if err != nil {
×
323
                                return spec, err
×
324
                        }
×
325

326
                        return spec, fmt.Errorf("release %s: %w", k, ErrReleaseNotRenderedYet)
×
327
                }
328

329
                if v.Status.ChartURL == "" {
18✔
330
                        return spec, fmt.Errorf("chartURL of release %s was empty, check the release's status", k)
×
331
                }
×
332

333
                ref, err := ociname.ParseReference(v.Status.ChartURL)
18✔
334
                if err != nil {
18✔
335
                        return spec, err
×
336
                }
×
337

338
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
18✔
339
                if err != nil {
18✔
340
                        return spec, err
×
341
                }
×
342

343
                resolvedReleases[k] = solarv1alpha1.ResourceAccess{
18✔
344
                        Repository: strings.TrimPrefix(repo, "oci://"),
18✔
345
                        Tag:        ref.Identifier(),
18✔
346
                }
18✔
347
        }
348

349
        resolvedReleaseNames := make([]string, 0, len(resolvedReleases))
16✔
350
        for k := range resolvedReleases {
34✔
351
                resolvedReleaseNames = append(resolvedReleaseNames, k)
18✔
352
        }
18✔
353

354
        chartName := fmt.Sprintf("bootstrap-%s", res.Name)
16✔
355
        repo, err := url.JoinPath(res.Namespace, chartName)
16✔
356
        if err != nil {
16✔
357
                return spec, err
×
358
        }
×
359

360
        tag := fmt.Sprintf("v0.0.%d", res.GetGeneration())
16✔
361

16✔
362
        spec.RendererConfig = solarv1alpha1.RendererConfig{
16✔
363
                Type: solarv1alpha1.RendererConfigTypeBootstrap,
16✔
364
                BootstrapConfig: solarv1alpha1.BootstrapConfig{
16✔
365
                        Chart: solarv1alpha1.ChartConfig{
16✔
366
                                Name:        chartName,
16✔
367
                                Description: fmt.Sprintf("Bootstrap of %v", resolvedReleaseNames),
16✔
368
                                Version:     tag,
16✔
369
                                AppVersion:  tag,
16✔
370
                        },
16✔
371
                        Input: solarv1alpha1.BootstrapInput{
16✔
372
                                Releases: resolvedReleases,
16✔
373
                                Userdata: res.Spec.Userdata,
16✔
374
                        },
16✔
375
                },
16✔
376
        }
16✔
377
        spec.Repository = repo
16✔
378
        spec.Tag = tag
16✔
379

16✔
380
        return spec, nil
16✔
381
}
382

383
// SetupWithManager sets up the controller with the Manager.
384
func (r *BootstrapReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
385
        return ctrl.NewControllerManagedBy(mgr).
1✔
386
                For(&solarv1alpha1.Bootstrap{}).
1✔
387
                Watches(&solarv1alpha1.RenderTask{},
1✔
388
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Bootstrap")),
1✔
389
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
390
                ).
1✔
391
                Complete(r)
1✔
392
}
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