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

opendefensecloud / solution-arsenal / 24348008499

13 Apr 2026 02:10PM UTC coverage: 72.293% (-2.0%) from 74.334%
24348008499

Pull #395

github

web-flow
Merge 533051a8b into 3bbb809bb
Pull Request #395: feat: split of additional resources from Target and refactor rendering

535 of 713 new or added lines in 8 files covered. (75.04%)

16 existing lines in 3 files now uncovered.

2030 of 2808 relevant lines covered (72.29%)

20.88 hits per line

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

75.74
/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) {
100✔
76
        log := ctrl.LoggerFrom(ctx)
100✔
77

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

100✔
80
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
130✔
81
                return ctrl.Result{}, nil
30✔
82
        }
30✔
83

84
        // Fetch target
85
        target := &solarv1alpha1.Target{}
70✔
86
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
71✔
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() {
70✔
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) {
83✔
125
                latest := &solarv1alpha1.Target{}
15✔
126
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
15✔
NEW
127
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
128
                }
×
129

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

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

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

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

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

157
        if registry.Spec.SolarSecretRef == nil {
34✔
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",
30✔
167
                "Registry resolved: "+registry.Name); condErr != nil {
30✔
NEW
168
                return ctrl.Result{}, condErr
×
NEW
169
        }
×
170

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

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

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

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

23✔
193
        pendingDeps := false
23✔
194

23✔
195
        for _, binding := range bindingList.Items {
55✔
196
                rel := &solarv1alpha1.Release{}
32✔
197
                if err := r.Get(ctx, client.ObjectKey{
32✔
198
                        Name:      binding.Spec.ReleaseRef.Name,
32✔
199
                        Namespace: target.Namespace,
32✔
200
                }, rel); err != nil {
32✔
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{}
32✔
212
                if err := r.Get(ctx, client.ObjectKey{
32✔
213
                        Name:      rel.Spec.ComponentVersionRef.Name,
32✔
214
                        Namespace: target.Namespace,
32✔
215
                }, cv); err != nil {
32✔
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())
32✔
227
                releases = append(releases, releaseInfo{
32✔
228
                        name:    rel.Name,
32✔
229
                        release: rel,
32✔
230
                        cv:      cv,
32✔
231
                        rtName:  rtName,
32✔
232
                })
32✔
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
23✔
238

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

32✔
243
                if apierrors.IsNotFound(err) {
35✔
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 {
29✔
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) {
32✔
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 != "" {
53✔
275
                        releases[i].chartURL = rt.Status.ChartURL
21✔
276
                } else {
32✔
277
                        allRendered = false
11✔
278
                }
11✔
279
        }
280

281
        if pendingDeps {
23✔
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",
11✔
292
                        "Waiting for release RenderTasks to complete"); condErr != nil {
11✔
NEW
293
                        return ctrl.Result{}, condErr
×
NEW
294
                }
×
295

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

299
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
12✔
300
                "All releases rendered successfully"); condErr != nil {
12✔
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
12✔
307
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
12✔
308
        bootstrapRT := &solarv1alpha1.RenderTask{}
12✔
309
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
12✔
310

12✔
311
        needsNewBootstrap := false
12✔
312

12✔
313
        switch {
12✔
314
        case apierrors.IsNotFound(err):
2✔
315
                // No RenderTask for the current version yet — create one
2✔
316
                needsNewBootstrap = true
2✔
NEW
317
        case err != nil:
×
NEW
318
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
319
        default:
10✔
320
                // RenderTask exists — check if the desired bootstrap input changed
10✔
321
                // (release set, resolved refs/tags, or userdata)
10✔
322
                desiredInput, inputErr := buildBootstrapInput(target, releases)
10✔
323
                if inputErr != nil {
10✔
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
10✔
328
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
12✔
329
                        bootstrapVersion++
2✔
330
                        needsNewBootstrap = true
2✔
331
                }
2✔
332
        }
333

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

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

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

354
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
2✔
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 {
6✔
365
                        target.Status.BootstrapVersion = bootstrapVersion
2✔
366
                        if err := r.Status().Update(ctx, target); err != nil {
3✔
367
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
1✔
368
                        }
1✔
369
                }
370
        }
371

372
        // Update target status from bootstrap RenderTask
373
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
11✔
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) {
16✔
383
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
5✔
384
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
5✔
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: {}}
5✔
390
                for _, ri := range releases {
13✔
391
                        currentRTNames[ri.rtName] = struct{}{}
8✔
392
                }
8✔
393
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
5✔
NEW
394
                        log.Error(err, "failed to clean up stale RenderTasks")
×
NEW
395
                }
×
396

397
                return ctrl.Result{}, nil
5✔
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 {
88✔
405
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
88✔
406
                Type:               condType,
88✔
407
                Status:             status,
88✔
408
                ObservedGeneration: target.Generation,
88✔
409
                Reason:             reason,
88✔
410
                Message:            message,
88✔
411
        })
88✔
412
        if changed {
112✔
413
                if err := r.Status().Update(ctx, target); err != nil {
24✔
NEW
414
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
×
NEW
415
                }
×
416
        }
417

418
        return nil
88✔
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 {
5✔
425
        log := ctrl.LoggerFrom(ctx)
5✔
426

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

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

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

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

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

454
        return nil
5✔
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) {
14✔
515
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
14✔
516

14✔
517
        for _, ri := range releases {
36✔
518
                ref, err := ociname.ParseReference(ri.chartURL)
22✔
519
                if err != nil {
22✔
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())
22✔
524
                if err != nil {
22✔
NEW
525
                        return solarv1alpha1.BootstrapInput{}, err
×
NEW
526
                }
×
527

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

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

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

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

551
        sort.Strings(releaseNames)
4✔
552

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

4✔
557
        return solarv1alpha1.RenderTaskSpec{
4✔
558
                RendererConfig: solarv1alpha1.RendererConfig{
4✔
559
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
4✔
560
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
4✔
561
                                Chart: solarv1alpha1.ChartConfig{
4✔
562
                                        Name:        chartName,
4✔
563
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
4✔
564
                                        Version:     tag,
4✔
565
                                        AppVersion:  tag,
4✔
566
                                },
4✔
567
                                Input: input,
4✔
568
                        },
4✔
569
                },
4✔
570
                Repository:     repo,
4✔
571
                Tag:            tag,
4✔
572
                BaseURL:        registry.Spec.Hostname,
4✔
573
                PushSecretRef:  registry.Spec.SolarSecretRef,
4✔
574
                OwnerName:      target.Name,
4✔
575
                OwnerNamespace: target.Namespace,
4✔
576
                OwnerKind:      "Target",
4✔
577
        }, nil
4✔
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 {
6✔
NEW
621
                if t.Spec.RenderRegistryRef.Name == reg.Name {
×
NEW
622
                        requests = append(requests, reconcile.Request{
×
NEW
623
                                NamespacedName: types.NamespacedName{
×
NEW
624
                                        Name:      t.Name,
×
NEW
625
                                        Namespace: t.Namespace,
×
NEW
626
                                },
×
NEW
627
                        })
×
NEW
628
                }
×
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