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

opendefensecloud / solution-arsenal / 24986840838

27 Apr 2026 09:19AM UTC coverage: 72.466% (+0.04%) from 72.431%
24986840838

push

github

web-flow
feat: improve dev flow (#451)

- migrate makefile to the new
[dev-kit](https://github.com/opendefensecloud/dev-kit/)
- migrate flake to reference dev-kit

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

* **Chores**
* Centralized build/tool management via shared infrastructure and pinned
toolkit.
* Switched local environment setup to flake-based usage and removed
legacy hook cleanup.
  * Simplified CI test invocation and updated ignore rules.

* **Tests**
* Tests now resolve tooling via environment-aware helpers instead of
hardcoded paths.

* **Documentation**
* Updated walkthrough CLI example to reference the revised binary path.

* **Style**
  * Minor copyright wording update.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

2066 of 2851 relevant lines covered (72.47%)

21.51 hits per line

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

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

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

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

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

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

94
        // Handle deletion
95
        if !target.DeletionTimestamp.IsZero() {
65✔
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✔
101
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
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✔
108
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
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✔
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) {
77✔
125
                latest := &solarv1alpha1.Target{}
14✔
126
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
14✔
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✔
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{}
49✔
141
        if err := r.Get(ctx, client.ObjectKey{
49✔
142
                Name:      target.Spec.RenderRegistryRef.Name,
49✔
143
                Namespace: target.Namespace,
49✔
144
        }, registry); err != nil {
70✔
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 {
22✔
148
                                return ctrl.Result{}, condErr
1✔
149
                        }
1✔
150

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

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

157
        if registry.Spec.SolarSecretRef == nil {
30✔
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✔
160
                        return ctrl.Result{}, condErr
×
161
                }
×
162

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

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

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

180
        if len(bindingList.Items) == 0 {
33✔
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 {
8✔
184
                        return ctrl.Result{}, condErr
1✔
185
                }
1✔
186

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

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

19✔
193
        pendingDeps := false
19✔
194

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

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

27✔
243
                if apierrors.IsNotFound(err) {
30✔
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✔
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)
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 {
24✔
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) {
27✔
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 != "" {
45✔
275
                        releases[i].chartURL = rt.Status.ChartURL
18✔
276
                } else {
27✔
277
                        allRendered = false
9✔
278
                }
9✔
279
        }
280

281
        if pendingDeps {
19✔
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 {
28✔
291
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
9✔
292
                        "Waiting for release RenderTasks to complete"); condErr != nil {
9✔
293
                        return ctrl.Result{}, condErr
×
294
                }
×
295

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

299
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
10✔
300
                "All releases rendered successfully"); condErr != nil {
10✔
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
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✔
317
        case err != nil:
×
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✔
324
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
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✔
337
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
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) {
×
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 {
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✔
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) {
10✔
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) {
14✔
383
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
4✔
384
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
4✔
385
                        return ctrl.Result{}, condErr
×
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✔
394
                        log.Error(err, "failed to clean up stale RenderTasks")
×
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 {
79✔
405
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
79✔
406
                Type:               condType,
79✔
407
                Status:             status,
79✔
408
                ObservedGeneration: target.Generation,
79✔
409
                Reason:             reason,
79✔
410
                Message:            message,
79✔
411
        })
79✔
412
        if changed {
105✔
413
                if err := r.Status().Update(ctx, target); err != nil {
28✔
414
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
2✔
415
                }
2✔
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✔
432
                return err
×
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✔
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✔
447
                        return err
×
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✔
463
                return err
×
464
        }
×
465

466
        for i := range rtList.Items {
1✔
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
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
        var targetNamespace string
3✔
484
        if rel.Spec.TargetNamespace != nil {
6✔
485
                targetNamespace = *rel.Spec.TargetNamespace
3✔
486
        }
3✔
487

488
        return solarv1alpha1.RenderTaskSpec{
3✔
489
                RendererConfig: solarv1alpha1.RendererConfig{
3✔
490
                        Type: solarv1alpha1.RendererConfigTypeRelease,
3✔
491
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
3✔
492
                                Chart: solarv1alpha1.ChartConfig{
3✔
493
                                        Name:        chartName,
3✔
494
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
3✔
495
                                        Version:     tag,
3✔
496
                                        AppVersion:  tag,
3✔
497
                                },
3✔
498
                                Input: solarv1alpha1.ReleaseInput{
3✔
499
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
3✔
500
                                        Resources:  cv.Spec.Resources,
3✔
501
                                        Entrypoint: cv.Spec.Entrypoint,
3✔
502
                                },
3✔
503
                                Values:          rel.Spec.Values,
3✔
504
                                TargetNamespace: targetNamespace,
3✔
505
                        },
3✔
506
                },
3✔
507
                Repository:     repo,
3✔
508
                Tag:            tag,
3✔
509
                BaseURL:        registry.Spec.Hostname,
3✔
510
                PushSecretRef:  registry.Spec.SolarSecretRef,
3✔
511
                FailedJobTTL:   rel.Spec.FailedJobTTL,
3✔
512
                OwnerName:      target.Name,
3✔
513
                OwnerNamespace: target.Namespace,
3✔
514
                OwnerKind:      "Target",
3✔
515
        }
3✔
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) {
11✔
521
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
11✔
522

11✔
523
        for _, ri := range releases {
28✔
524
                ref, err := ociname.ParseReference(ri.chartURL)
17✔
525
                if err != nil {
17✔
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())
17✔
530
                if err != nil {
17✔
531
                        return solarv1alpha1.BootstrapInput{}, err
×
532
                }
×
533

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

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

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

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

557
        sort.Strings(releaseNames)
2✔
558

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

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

586
// SetupWithManager sets up the controller with the Manager.
587
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
588
        return ctrl.NewControllerManagedBy(mgr).
1✔
589
                For(&solarv1alpha1.Target{}).
1✔
590
                Watches(
1✔
591
                        &solarv1alpha1.ReleaseBinding{},
1✔
592
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
593
                ).
1✔
594
                Watches(
1✔
595
                        &solarv1alpha1.RenderTask{},
1✔
596
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
597
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
598
                ).
1✔
599
                Watches(
1✔
600
                        &solarv1alpha1.Registry{},
1✔
601
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
602
                ).
1✔
603
                Watches(
1✔
604
                        &solarv1alpha1.Release{},
1✔
605
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
606
                ).
1✔
607
                Complete(r)
1✔
608
}
1✔
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 {
6✔
613
        reg, ok := obj.(*solarv1alpha1.Registry)
6✔
614
        if !ok {
6✔
615
                return nil
×
616
        }
×
617

618
        targetList := &solarv1alpha1.TargetList{}
6✔
619
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
6✔
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
6✔
626
        for _, t := range targetList.Items {
7✔
627
                if t.Spec.RenderRegistryRef.Name == reg.Name {
2✔
628
                        requests = append(requests, reconcile.Request{
1✔
629
                                NamespacedName: types.NamespacedName{
1✔
630
                                        Name:      t.Name,
1✔
631
                                        Namespace: t.Namespace,
1✔
632
                                },
1✔
633
                        })
1✔
634
                }
1✔
635
        }
636

637
        return requests
6✔
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 {
15✔
643
        rel, ok := obj.(*solarv1alpha1.Release)
15✔
644
        if !ok {
15✔
645
                return nil
×
646
        }
×
647

648
        bindingList := &solarv1alpha1.ReleaseBindingList{}
15✔
649
        if err := r.List(ctx, bindingList,
15✔
650
                client.InNamespace(rel.Namespace),
15✔
651
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
15✔
652
        ); err != nil {
15✔
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{}{}
15✔
659
        var requests []reconcile.Request
15✔
660

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

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

676
        return requests
15✔
677
}
678

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

685
        return []reconcile.Request{
10✔
686
                {
10✔
687
                        NamespacedName: types.NamespacedName{
10✔
688
                                Name:      rb.Spec.TargetRef.Name,
10✔
689
                                Namespace: rb.Namespace,
10✔
690
                        },
10✔
691
                },
10✔
692
        }
10✔
693
}
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