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

opendefensecloud / solution-arsenal / 24348097381

13 Apr 2026 02:11PM UTC coverage: 72.65% (-1.7%) from 74.334%
24348097381

push

github

web-flow
feat: split of additional resources from Target and refactor rendering (#395)

Closes #386.
Closes #387.
Closes #388.
Closes #389.
Closes #390.

Conflicting with
https://github.com/opendefensecloud/solution-arsenal/pull/355 however!

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

* **New Features**
* Added Registry, RegistryBinding, ReleaseBinding and Profile resources;
Profile auto-creates ReleaseBindings.
  * New standalone solar-discovery Helm chart (scan & webhook modes).

* **Changes**
* Targets now reference a render registry; RenderTasks are namespaced
and carry per-task baseURL/pushSecretRef.
* New Profile and Target controller behaviors to manage ReleaseBindings
and per-release/bootstrap rendering.

* **Removals**
  * Bootstrap and Discovery APIs/controllers removed.

* **Documentation**
* Added rendering-pipeline and updated architecture and discovery
guides.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

546 of 713 new or added lines in 8 files covered. (76.58%)

16 existing lines in 3 files now uncovered.

2040 of 2808 relevant lines covered (72.65%)

17.01 hits per line

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

77.45
/pkg/controller/target_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
        "sort"
13
        "strings"
14
        "time"
15

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

31
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
32
)
33

34
const (
35
        targetFinalizer = "solar.opendefense.cloud/target-finalizer"
36

37
        ConditionTypeRegistryResolved = "RegistryResolved"
38
        ConditionTypeReleasesRendered = "ReleasesRendered"
39
        ConditionTypeBootstrapReady   = "BootstrapReady"
40
)
41

42
var ErrReleaseNotRenderedYet = errors.New("release is not rendered yet")
43

44
type releaseInfo struct {
45
        name     string
46
        release  *solarv1alpha1.Release
47
        cv       *solarv1alpha1.ComponentVersion
48
        rtName   string
49
        chartURL string
50
}
51

52
type TargetReconciler struct {
53
        client.Client
54
        Scheme   *runtime.Scheme
55
        Recorder events.EventRecorder
56
        // WatchNamespace restricts reconciliation to this namespace.
57
        // Should be empty in production (watches all namespaces).
58
        // Intended for use in integration tests only.
59
        WatchNamespace string
60
}
61

62
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete
63
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch
64
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update
65
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get;list;watch
66
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch
67
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
68
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
69
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
70
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
71
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
72

73
// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release
74
// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask.
75
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
94✔
76
        log := ctrl.LoggerFrom(ctx)
94✔
77

94✔
78
        log.V(1).Info("Target is being reconciled", "req", req)
94✔
79

94✔
80
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
126✔
81
                return ctrl.Result{}, nil
32✔
82
        }
32✔
83

84
        // Fetch target
85
        target := &solarv1alpha1.Target{}
62✔
86
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
63✔
87
                if apierrors.IsNotFound(err) {
2✔
88
                        return ctrl.Result{}, nil
1✔
89
                }
1✔
90

NEW
91
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
92
        }
93

94
        // Handle deletion
95
        if !target.DeletionTimestamp.IsZero() {
62✔
96
                log.V(1).Info("Target is being deleted")
1✔
97
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
1✔
98

1✔
99
                // Delete owned RenderTasks
1✔
100
                if err := r.deleteOwnedRenderTasks(ctx, target); err != nil {
1✔
NEW
101
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
UNCOV
102
                }
×
103

104
                // Remove finalizer
105
                if slices.Contains(target.Finalizers, targetFinalizer) {
2✔
106
                        latest := &solarv1alpha1.Target{}
1✔
107
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
1✔
NEW
108
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
UNCOV
109
                        }
×
110

111
                        original := latest.DeepCopy()
1✔
112
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
2✔
113
                                return s == targetFinalizer
1✔
114
                        })
1✔
115
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
1✔
NEW
116
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
117
                        }
×
118
                }
119

120
                return ctrl.Result{}, nil
1✔
121
        }
122

123
        // Set finalizer if not set
124
        if !slices.Contains(target.Finalizers, targetFinalizer) {
74✔
125
                latest := &solarv1alpha1.Target{}
14✔
126
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
14✔
NEW
127
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
128
                }
×
129

130
                original := latest.DeepCopy()
14✔
131
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
14✔
132
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
14✔
NEW
133
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
134
                }
×
135

136
                return ctrl.Result{}, nil
14✔
137
        }
138

139
        // Resolve render registry
140
        registry := &solarv1alpha1.Registry{}
46✔
141
        if err := r.Get(ctx, client.ObjectKey{
46✔
142
                Name:      target.Spec.RenderRegistryRef.Name,
46✔
143
                Namespace: target.Namespace,
46✔
144
        }, registry); err != nil {
62✔
145
                if apierrors.IsNotFound(err) {
32✔
146
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
16✔
147
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
16✔
NEW
148
                                return ctrl.Result{}, condErr
×
NEW
149
                        }
×
150

151
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
16✔
152
                }
153

NEW
154
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
155
        }
156

157
        if registry.Spec.SolarSecretRef == nil {
32✔
158
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
159
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
NEW
160
                        return ctrl.Result{}, condErr
×
NEW
161
                }
×
162

163
                return ctrl.Result{}, nil
2✔
164
        }
165

166
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
28✔
167
                "Registry resolved: "+registry.Name); condErr != nil {
28✔
NEW
168
                return ctrl.Result{}, condErr
×
NEW
169
        }
×
170

171
        // Collect ReleaseBindings for this target
172
        bindingList := &solarv1alpha1.ReleaseBindingList{}
28✔
173
        if err := r.List(ctx, bindingList,
28✔
174
                client.InNamespace(target.Namespace),
28✔
175
                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
28✔
176
        ); err != nil {
28✔
NEW
177
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
NEW
178
        }
×
179

180
        if len(bindingList.Items) == 0 {
34✔
181
                log.V(1).Info("No ReleaseBindings found for target")
6✔
182
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoBindings",
6✔
183
                        "No ReleaseBindings found for this target"); condErr != nil {
6✔
NEW
184
                        return ctrl.Result{}, condErr
×
NEW
185
                }
×
186

187
                return ctrl.Result{}, nil
6✔
188
        }
189

190
        // For each bound release, ensure a per-release RenderTask exists
191
        var releases []releaseInfo
22✔
192

22✔
193
        pendingDeps := false
22✔
194

22✔
195
        for _, binding := range bindingList.Items {
53✔
196
                rel := &solarv1alpha1.Release{}
31✔
197
                if err := r.Get(ctx, client.ObjectKey{
31✔
198
                        Name:      binding.Spec.ReleaseRef.Name,
31✔
199
                        Namespace: target.Namespace,
31✔
200
                }, rel); err != nil {
31✔
NEW
201
                        if apierrors.IsNotFound(err) {
×
NEW
202
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
NEW
203
                                pendingDeps = true
×
NEW
204

×
NEW
205
                                continue
×
206
                        }
207

NEW
208
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
209
                }
210

211
                cv := &solarv1alpha1.ComponentVersion{}
31✔
212
                if err := r.Get(ctx, client.ObjectKey{
31✔
213
                        Name:      rel.Spec.ComponentVersionRef.Name,
31✔
214
                        Namespace: target.Namespace,
31✔
215
                }, cv); err != nil {
31✔
NEW
216
                        if apierrors.IsNotFound(err) {
×
NEW
217
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
NEW
218
                                pendingDeps = true
×
NEW
219

×
NEW
220
                                continue
×
221
                        }
222

NEW
223
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
224
                }
225

226
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
31✔
227
                releases = append(releases, releaseInfo{
31✔
228
                        name:    rel.Name,
31✔
229
                        release: rel,
31✔
230
                        cv:      cv,
31✔
231
                        rtName:  rtName,
31✔
232
                })
31✔
233
        }
234

235
        // Create per-release RenderTasks (one per target+release pair).
236
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
237
        allRendered := true
22✔
238

22✔
239
        for i, ri := range releases {
53✔
240
                rt := &solarv1alpha1.RenderTask{}
31✔
241
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
31✔
242

31✔
243
                if apierrors.IsNotFound(err) {
34✔
244
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
3✔
245
                        rt = &solarv1alpha1.RenderTask{
3✔
246
                                ObjectMeta: metav1.ObjectMeta{
3✔
247
                                        Name:      ri.rtName,
3✔
248
                                        Namespace: target.Namespace,
3✔
249
                                },
3✔
250
                                Spec: spec,
3✔
251
                        }
3✔
252

3✔
253
                        if err := r.Create(ctx, rt); err != nil {
3✔
NEW
254
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
NEW
255
                        }
×
256

257
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
3✔
258
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
3✔
259
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
3✔
260
                } else if err != nil {
28✔
NEW
261
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
NEW
262
                }
×
263

264
                // Check if release RenderTask is complete
265
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
31✔
NEW
266
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
NEW
267
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
NEW
268
                                return ctrl.Result{}, condErr
×
NEW
269
                        }
×
270

NEW
271
                        return ctrl.Result{}, nil
×
272
                }
273

274
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
50✔
275
                        releases[i].chartURL = rt.Status.ChartURL
19✔
276
                } else {
31✔
277
                        allRendered = false
12✔
278
                }
12✔
279
        }
280

281
        if pendingDeps {
22✔
NEW
282
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
NEW
283
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
NEW
284
                        return ctrl.Result{}, condErr
×
NEW
285
                }
×
286

NEW
287
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
288
        }
289

290
        if !allRendered {
34✔
291
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
12✔
292
                        "Waiting for release RenderTasks to complete"); condErr != nil {
13✔
293
                        return ctrl.Result{}, condErr
1✔
294
                }
1✔
295

296
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
11✔
297
        }
298

299
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
10✔
300
                "All releases rendered successfully"); condErr != nil {
10✔
NEW
301
                return ctrl.Result{}, condErr
×
NEW
302
        }
×
303

304
        // Determine if a new bootstrap render is needed by checking whether the
305
        // current bootstrapVersion's RenderTask still matches the desired release set.
306
        bootstrapVersion := target.Status.BootstrapVersion
10✔
307
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
10✔
308
        bootstrapRT := &solarv1alpha1.RenderTask{}
10✔
309
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
10✔
310

10✔
311
        needsNewBootstrap := false
10✔
312

10✔
313
        switch {
10✔
314
        case apierrors.IsNotFound(err):
1✔
315
                // No RenderTask for the current version yet — create one
1✔
316
                needsNewBootstrap = true
1✔
NEW
317
        case err != nil:
×
NEW
318
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
319
        default:
9✔
320
                // RenderTask exists — check if the desired bootstrap input changed
9✔
321
                // (release set, resolved refs/tags, or userdata)
9✔
322
                desiredInput, inputErr := buildBootstrapInput(target, releases)
9✔
323
                if inputErr != nil {
9✔
NEW
324
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
NEW
325
                }
×
326

327
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
9✔
328
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
10✔
329
                        bootstrapVersion++
1✔
330
                        needsNewBootstrap = true
1✔
331
                }
1✔
332
        }
333

334
        if needsNewBootstrap {
12✔
335
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
336
                if specErr != nil {
2✔
NEW
337
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
NEW
338
                }
×
339

340
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
341
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
342
                        ObjectMeta: metav1.ObjectMeta{
2✔
343
                                Name:      bootstrapRTName,
2✔
344
                                Namespace: target.Namespace,
2✔
345
                        },
2✔
346
                        Spec: spec,
2✔
347
                }
2✔
348

2✔
349
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
350
                        if !apierrors.IsAlreadyExists(err) {
×
NEW
351
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
NEW
352
                        }
×
353

NEW
354
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
NEW
355
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
356
                        }
×
357
                } else {
2✔
358
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
359
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
360
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
361
                }
2✔
362

363
                // Persist the new bootstrapVersion in status
364
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
365
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
366
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
NEW
367
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
NEW
368
                        }
×
369
                }
370
        }
371

372
        // Update target status from bootstrap RenderTask
373
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
10✔
NEW
374
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
NEW
375
                        "Bootstrap rendering failed"); condErr != nil {
×
NEW
376
                        return ctrl.Result{}, condErr
×
NEW
377
                }
×
378

NEW
379
                return ctrl.Result{}, nil
×
380
        }
381

382
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
14✔
383
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
4✔
384
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
4✔
NEW
385
                        return ctrl.Result{}, condErr
×
NEW
386
                }
×
387

388
                // Clean up stale RenderTasks owned by this target (old versions)
389
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
4✔
390
                for _, ri := range releases {
10✔
391
                        currentRTNames[ri.rtName] = struct{}{}
6✔
392
                }
6✔
393
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
4✔
NEW
394
                        log.Error(err, "failed to clean up stale RenderTasks")
×
NEW
395
                }
×
396

397
                return ctrl.Result{}, nil
4✔
398
        }
399

400
        // Still running
401
        return ctrl.Result{}, nil
6✔
402
}
403

404
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
78✔
405
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
78✔
406
                Type:               condType,
78✔
407
                Status:             status,
78✔
408
                ObservedGeneration: target.Generation,
78✔
409
                Reason:             reason,
78✔
410
                Message:            message,
78✔
411
        })
78✔
412
        if changed {
103✔
413
                if err := r.Status().Update(ctx, target); err != nil {
26✔
414
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
1✔
415
                }
1✔
416
        }
417

418
        return nil
77✔
419
}
420

421
// deleteStaleRenderTasks removes RenderTasks owned by this target that are no
422
// longer needed. Any owned RenderTask whose name is not in currentRTNames is
423
// deleted. This covers both old bootstrap versions and old release generations.
424
func (r *TargetReconciler) deleteStaleRenderTasks(ctx context.Context, target *solarv1alpha1.Target, currentRTNames map[string]struct{}) error {
4✔
425
        log := ctrl.LoggerFrom(ctx)
4✔
426

4✔
427
        rtList := &solarv1alpha1.RenderTaskList{}
4✔
428
        if err := r.List(ctx, rtList,
4✔
429
                client.InNamespace(target.Namespace),
4✔
430
                client.MatchingFields{indexOwnerKind: "Target"},
4✔
431
        ); err != nil {
4✔
NEW
432
                return err
×
NEW
433
        }
×
434

435
        for i := range rtList.Items {
15✔
436
                rt := &rtList.Items[i]
11✔
437
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
11✔
NEW
438
                        continue
×
439
                }
440

441
                if _, current := currentRTNames[rt.Name]; current {
21✔
442
                        continue
10✔
443
                }
444

445
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
446
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
NEW
447
                        return err
×
NEW
448
                }
×
449

450
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
451
                        "Deleted stale RenderTask %s", rt.Name)
1✔
452
        }
453

454
        return nil
4✔
455
}
456

457
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
458
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
459
        if err := r.List(ctx, rtList,
1✔
460
                client.InNamespace(target.Namespace),
1✔
461
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
462
        ); err != nil {
1✔
NEW
463
                return err
×
NEW
464
        }
×
465

466
        for i := range rtList.Items {
1✔
NEW
467
                rt := &rtList.Items[i]
×
NEW
468
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
NEW
469
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
NEW
470
                                return err
×
NEW
471
                        }
×
472
                }
473
        }
474

475
        return nil
1✔
476
}
477

478
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec {
3✔
479
        chartName := fmt.Sprintf("release-%s", rel.Name)
3✔
480
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
3✔
481
        tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration())
3✔
482

3✔
483
        return solarv1alpha1.RenderTaskSpec{
3✔
484
                RendererConfig: solarv1alpha1.RendererConfig{
3✔
485
                        Type: solarv1alpha1.RendererConfigTypeRelease,
3✔
486
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
3✔
487
                                Chart: solarv1alpha1.ChartConfig{
3✔
488
                                        Name:        chartName,
3✔
489
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
3✔
490
                                        Version:     tag,
3✔
491
                                        AppVersion:  tag,
3✔
492
                                },
3✔
493
                                Input: solarv1alpha1.ReleaseInput{
3✔
494
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
3✔
495
                                        Resources:  cv.Spec.Resources,
3✔
496
                                        Entrypoint: cv.Spec.Entrypoint,
3✔
497
                                },
3✔
498
                                Values: rel.Spec.Values,
3✔
499
                        },
3✔
500
                },
3✔
501
                Repository:     repo,
3✔
502
                Tag:            tag,
3✔
503
                BaseURL:        registry.Spec.Hostname,
3✔
504
                PushSecretRef:  registry.Spec.SolarSecretRef,
3✔
505
                FailedJobTTL:   rel.Spec.FailedJobTTL,
3✔
506
                OwnerName:      target.Name,
3✔
507
                OwnerNamespace: target.Namespace,
3✔
508
                OwnerKind:      "Target",
3✔
509
        }
3✔
510
}
3✔
511

512
// buildBootstrapInput constructs the desired BootstrapInput from the current
513
// target and resolved releases. Used for both comparison and spec construction.
514
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
11✔
515
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
11✔
516

11✔
517
        for _, ri := range releases {
28✔
518
                ref, err := ociname.ParseReference(ri.chartURL)
17✔
519
                if err != nil {
17✔
NEW
520
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
NEW
521
                }
×
522

523
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
17✔
524
                if err != nil {
17✔
NEW
525
                        return solarv1alpha1.BootstrapInput{}, err
×
NEW
526
                }
×
527

528
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
17✔
529
                        Repository: strings.TrimPrefix(repo, "oci://"),
17✔
530
                        Tag:        ref.Identifier(),
17✔
531
                }
17✔
532
        }
533

534
        return solarv1alpha1.BootstrapInput{
11✔
535
                Releases: resolvedReleases,
11✔
536
                Userdata: target.Spec.Userdata,
11✔
537
        }, nil
11✔
538
}
539

540
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
2✔
541
        input, err := buildBootstrapInput(target, releases)
2✔
542
        if err != nil {
2✔
NEW
543
                return solarv1alpha1.RenderTaskSpec{}, err
×
NEW
544
        }
×
545

546
        releaseNames := make([]string, 0, len(releases))
2✔
547
        for _, ri := range releases {
5✔
548
                releaseNames = append(releaseNames, ri.name)
3✔
549
        }
3✔
550

551
        sort.Strings(releaseNames)
2✔
552

2✔
553
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
2✔
554
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
2✔
555
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
2✔
556

2✔
557
        return solarv1alpha1.RenderTaskSpec{
2✔
558
                RendererConfig: solarv1alpha1.RendererConfig{
2✔
559
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
2✔
560
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
2✔
561
                                Chart: solarv1alpha1.ChartConfig{
2✔
562
                                        Name:        chartName,
2✔
563
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
2✔
564
                                        Version:     tag,
2✔
565
                                        AppVersion:  tag,
2✔
566
                                },
2✔
567
                                Input: input,
2✔
568
                        },
2✔
569
                },
2✔
570
                Repository:     repo,
2✔
571
                Tag:            tag,
2✔
572
                BaseURL:        registry.Spec.Hostname,
2✔
573
                PushSecretRef:  registry.Spec.SolarSecretRef,
2✔
574
                OwnerName:      target.Name,
2✔
575
                OwnerNamespace: target.Namespace,
2✔
576
                OwnerKind:      "Target",
2✔
577
        }, nil
2✔
578
}
579

580
// SetupWithManager sets up the controller with the Manager.
581
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
582
        return ctrl.NewControllerManagedBy(mgr).
1✔
583
                For(&solarv1alpha1.Target{}).
1✔
584
                Watches(
1✔
585
                        &solarv1alpha1.ReleaseBinding{},
1✔
586
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
587
                ).
1✔
588
                Watches(
1✔
589
                        &solarv1alpha1.RenderTask{},
1✔
590
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
591
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
592
                ).
1✔
593
                Watches(
1✔
594
                        &solarv1alpha1.Registry{},
1✔
595
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
596
                ).
1✔
597
                Watches(
1✔
598
                        &solarv1alpha1.Release{},
1✔
599
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
600
                ).
1✔
601
                Complete(r)
1✔
602
}
1✔
603

604
// mapRegistryToTargets maps a Registry event to reconcile requests for all
605
// Targets in the same namespace that reference it via renderRegistryRef.
606
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
6✔
607
        reg, ok := obj.(*solarv1alpha1.Registry)
6✔
608
        if !ok {
6✔
NEW
609
                return nil
×
UNCOV
610
        }
×
611

612
        targetList := &solarv1alpha1.TargetList{}
6✔
613
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
6✔
NEW
614
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
UNCOV
615

×
UNCOV
616
                return nil
×
UNCOV
617
        }
×
618

619
        var requests []reconcile.Request
6✔
620
        for _, t := range targetList.Items {
7✔
621
                if t.Spec.RenderRegistryRef.Name == reg.Name {
2✔
622
                        requests = append(requests, reconcile.Request{
1✔
623
                                NamespacedName: types.NamespacedName{
1✔
624
                                        Name:      t.Name,
1✔
625
                                        Namespace: t.Namespace,
1✔
626
                                },
1✔
627
                        })
1✔
628
                }
1✔
629
        }
630

631
        return requests
6✔
632
}
633

634
// mapReleaseToTargets maps a Release event to reconcile requests for all
635
// Targets that are bound to the release via ReleaseBindings.
636
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
15✔
637
        rel, ok := obj.(*solarv1alpha1.Release)
15✔
638
        if !ok {
15✔
639
                return nil
×
640
        }
×
641

642
        bindingList := &solarv1alpha1.ReleaseBindingList{}
15✔
643
        if err := r.List(ctx, bindingList,
15✔
644
                client.InNamespace(rel.Namespace),
15✔
645
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
15✔
646
        ); err != nil {
15✔
NEW
647
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
NEW
648

×
649
                return nil
×
650
        }
×
651

652
        seen := map[string]struct{}{}
15✔
653
        var requests []reconcile.Request
15✔
654

15✔
655
        for _, rb := range bindingList.Items {
17✔
656
                targetName := rb.Spec.TargetRef.Name
2✔
657
                if _, ok := seen[targetName]; ok {
2✔
NEW
658
                        continue
×
659
                }
660

661
                seen[targetName] = struct{}{}
2✔
662
                requests = append(requests, reconcile.Request{
2✔
663
                        NamespacedName: types.NamespacedName{
2✔
664
                                Name:      targetName,
2✔
665
                                Namespace: rb.Namespace,
2✔
666
                        },
2✔
667
                })
2✔
668
        }
669

670
        return requests
15✔
671
}
672

673
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
10✔
674
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
10✔
675
        if !ok || rb.Spec.TargetRef.Name == "" {
10✔
NEW
676
                return nil
×
NEW
677
        }
×
678

679
        return []reconcile.Request{
10✔
680
                {
10✔
681
                        NamespacedName: types.NamespacedName{
10✔
682
                                Name:      rb.Spec.TargetRef.Name,
10✔
683
                                Namespace: rb.Namespace,
10✔
684
                        },
10✔
685
                },
10✔
686
        }
10✔
687
}
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