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

opendefensecloud / solution-arsenal / 24657746340

20 Apr 2026 08:59AM UTC coverage: 72.084% (-0.5%) from 72.546%
24657746340

Pull #433

github

web-flow
Merge 46ff68e48 into 201c8f176
Pull Request #433: (do not merge): change coverage

2027 of 2812 relevant lines covered (72.08%)

44.09 hits per line

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

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

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

190✔
80
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
251✔
81
                return ctrl.Result{}, nil
61✔
82
        }
61✔
83

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

41✔
193
        pendingDeps := false
41✔
194

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

×
205
                                continue
×
206
                        }
207

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

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

×
220
                                continue
×
221
                        }
222

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

226
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
59✔
227
                releases = append(releases, releaseInfo{
59✔
228
                        name:    rel.Name,
59✔
229
                        release: rel,
59✔
230
                        cv:      cv,
59✔
231
                        rtName:  rtName,
59✔
232
                })
59✔
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
41✔
238

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

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

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

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

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

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

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

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

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

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

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

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

23✔
311
        needsNewBootstrap := false
23✔
312

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

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

334
        if needsNewBootstrap {
27✔
335
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
4✔
336
                if specErr != nil {
4✔
337
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
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 {
4✔
350
                        if !apierrors.IsAlreadyExists(err) {
×
351
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
352
                        }
×
353

354
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
355
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
356
                        }
×
357
                } else {
4✔
358
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
4✔
359
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
4✔
360
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
4✔
361
                }
4✔
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 {
2✔
367
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
368
                        }
×
369
                }
370
        }
371

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

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

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

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

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

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

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

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

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

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

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

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

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

454
        return nil
10✔
455
}
456

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

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

475
        return nil
2✔
476
}
477

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

6✔
483
        var targetNamespace string
6✔
484
        if rel.Spec.TargetNamespace != nil {
12✔
485
                targetNamespace = *rel.Spec.TargetNamespace
6✔
486
        }
6✔
487

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

518
// buildBootstrapInput constructs the desired BootstrapInput from the current
519
// target and resolved releases. Used for both comparison and spec construction.
520
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
25✔
521
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
25✔
522

25✔
523
        for _, ri := range releases {
64✔
524
                ref, err := ociname.ParseReference(ri.chartURL)
39✔
525
                if err != nil {
39✔
526
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
527
                }
×
528

529
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
39✔
530
                if err != nil {
39✔
531
                        return solarv1alpha1.BootstrapInput{}, err
×
532
                }
×
533

534
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
39✔
535
                        Repository: strings.TrimPrefix(repo, "oci://"),
39✔
536
                        Tag:        ref.Identifier(),
39✔
537
                }
39✔
538
        }
539

540
        return solarv1alpha1.BootstrapInput{
25✔
541
                Releases: resolvedReleases,
25✔
542
                Userdata: target.Spec.Userdata,
25✔
543
        }, nil
25✔
544
}
545

546
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
4✔
547
        input, err := buildBootstrapInput(target, releases)
4✔
548
        if err != nil {
4✔
549
                return solarv1alpha1.RenderTaskSpec{}, err
×
550
        }
×
551

552
        releaseNames := make([]string, 0, len(releases))
4✔
553
        for _, ri := range releases {
10✔
554
                releaseNames = append(releaseNames, ri.name)
6✔
555
        }
6✔
556

557
        sort.Strings(releaseNames)
4✔
558

4✔
559
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
4✔
560
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
4✔
561
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
4✔
562

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

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

610
// mapRegistryToTargets maps a Registry event to reconcile requests for all
611
// Targets in the same namespace that reference it via renderRegistryRef.
612
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
12✔
613
        reg, ok := obj.(*solarv1alpha1.Registry)
12✔
614
        if !ok {
12✔
615
                return nil
×
616
        }
×
617

618
        targetList := &solarv1alpha1.TargetList{}
12✔
619
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
12✔
620
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
621

×
622
                return nil
×
623
        }
×
624

625
        var requests []reconcile.Request
12✔
626
        for _, t := range targetList.Items {
12✔
627
                if t.Spec.RenderRegistryRef.Name == reg.Name {
×
628
                        requests = append(requests, reconcile.Request{
×
629
                                NamespacedName: types.NamespacedName{
×
630
                                        Name:      t.Name,
×
631
                                        Namespace: t.Namespace,
×
632
                                },
×
633
                        })
×
634
                }
×
635
        }
636

637
        return requests
12✔
638
}
639

640
// mapReleaseToTargets maps a Release event to reconcile requests for all
641
// Targets that are bound to the release via ReleaseBindings.
642
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
30✔
643
        rel, ok := obj.(*solarv1alpha1.Release)
30✔
644
        if !ok {
30✔
645
                return nil
×
646
        }
×
647

648
        bindingList := &solarv1alpha1.ReleaseBindingList{}
30✔
649
        if err := r.List(ctx, bindingList,
30✔
650
                client.InNamespace(rel.Namespace),
30✔
651
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
30✔
652
        ); err != nil {
30✔
653
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
654

×
655
                return nil
×
656
        }
×
657

658
        seen := map[string]struct{}{}
30✔
659
        var requests []reconcile.Request
30✔
660

30✔
661
        for _, rb := range bindingList.Items {
30✔
662
                targetName := rb.Spec.TargetRef.Name
×
663
                if _, ok := seen[targetName]; ok {
×
664
                        continue
×
665
                }
666

667
                seen[targetName] = struct{}{}
×
668
                requests = append(requests, reconcile.Request{
×
669
                        NamespacedName: types.NamespacedName{
×
670
                                Name:      targetName,
×
671
                                Namespace: rb.Namespace,
×
672
                        },
×
673
                })
×
674
        }
675

676
        return requests
30✔
677
}
678

679
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
20✔
680
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
20✔
681
        if !ok || rb.Spec.TargetRef.Name == "" {
20✔
682
                return nil
×
683
        }
×
684

685
        return []reconcile.Request{
20✔
686
                {
20✔
687
                        NamespacedName: types.NamespacedName{
20✔
688
                                Name:      rb.Spec.TargetRef.Name,
20✔
689
                                Namespace: rb.Namespace,
20✔
690
                        },
20✔
691
                },
20✔
692
        }
20✔
693
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc