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

opendefensecloud / solution-arsenal / 25150910053

30 Apr 2026 06:28AM UTC coverage: 72.877% (+0.9%) from 71.93%
25150910053

push

github

web-flow
fix: check errors being not found errors (#472)

## What
<!-- One sentence summary -->
Closes #374 

## Testing
This PR improves existing tests

## Checklist
- [x] Tests added/updated
- [x] No breaking changes (or upgrade path documented above)
- [x] Readable commit history (squashed and cleaned up as desired)
- [x] AI code review considered and comments resolved


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

## Summary by CodeRabbit

* **Tests**
* Improved test assertions to more accurately verify resource deletion
by specifically checking for deletion conditions rather than treating
general retrieval errors as confirmation of removal. Enhanced validation
consistency across multiple test scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

2077 of 2850 relevant lines covered (72.88%)

22.28 hits per line

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

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

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

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

84
        // Fetch target
85
        target := &solarv1alpha1.Target{}
63✔
86
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
64✔
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() {
63✔
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) {
75✔
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{}
47✔
141
        if err := r.Get(ctx, client.ObjectKey{
47✔
142
                Name:      target.Spec.RenderRegistryRef.Name,
47✔
143
                Namespace: target.Namespace,
47✔
144
        }, registry); err != nil {
68✔
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✔
148
                                return ctrl.Result{}, condErr
×
149
                        }
×
150

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

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

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

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

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

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

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

18✔
193
        pendingDeps := false
18✔
194

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

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

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

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

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

418
        return nil
74✔
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