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

opendefensecloud / solution-arsenal / 25382447889

05 May 2026 02:27PM UTC coverage: 70.588%. First build
25382447889

Pull #494

github

web-flow
Merge 4a852e033 into 73a766a0e
Pull Request #494: feat(controller): implement release resolver in Target controller (#246)

80 of 87 new or added lines in 1 file covered. (91.95%)

2316 of 3281 relevant lines covered (70.59%)

19.7 hits per line

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

66.33
/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/labels"
23
        "k8s.io/apimachinery/pkg/runtime"
24
        "k8s.io/apimachinery/pkg/types"
25
        "k8s.io/client-go/tools/events"
26
        ctrl "sigs.k8s.io/controller-runtime"
27
        "sigs.k8s.io/controller-runtime/pkg/builder"
28
        "sigs.k8s.io/controller-runtime/pkg/client"
29
        "sigs.k8s.io/controller-runtime/pkg/handler"
30
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
31

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

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

38
        ConditionTypeRegistryResolved = "RegistryResolved"
39
        ConditionTypeReleasesResolved = "ReleasesResolved"
40
        ConditionTypeReleasesRendered = "ReleasesRendered"
41
        ConditionTypeBootstrapReady   = "BootstrapReady"
42
)
43

44
var ErrReleaseNotRenderedYet = errors.New("release is not rendered yet")
45

46
type releaseInfo struct {
47
        // bindingKey is "<namespace>/<name>" of the originating ReleaseBinding, used as a
48
        // deterministic tiebreaker when two releases share the same priority.
49
        bindingKey string
50
        name       string
51
        release    *solarv1alpha1.Release
52
        cv         *solarv1alpha1.ComponentVersion
53
        rtName     string
54
        chartURL   string
55
}
56

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

67
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete
68
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch
69
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update
70
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get;list;watch
71
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch
72
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
73
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
74
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list;watch
75
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
76
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
77
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
78

79
// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release
80
// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask.
81
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
128✔
82
        log := ctrl.LoggerFrom(ctx)
128✔
83

128✔
84
        log.V(1).Info("Target is being reconciled", "req", req)
128✔
85

128✔
86
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
172✔
87
                return ctrl.Result{}, nil
44✔
88
        }
44✔
89

90
        // Fetch target
91
        target := &solarv1alpha1.Target{}
84✔
92
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
91✔
93
                if apierrors.IsNotFound(err) {
14✔
94
                        return ctrl.Result{}, nil
7✔
95
                }
7✔
96

97
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
98
        }
99

100
        // Handle deletion
101
        if !target.DeletionTimestamp.IsZero() {
78✔
102
                log.V(1).Info("Target is being deleted")
1✔
103
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
1✔
104

1✔
105
                // Delete owned RenderTasks
1✔
106
                if err := r.deleteOwnedRenderTasks(ctx, target); err != nil {
1✔
107
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
108
                }
×
109

110
                // Remove finalizer
111
                if slices.Contains(target.Finalizers, targetFinalizer) {
2✔
112
                        latest := &solarv1alpha1.Target{}
1✔
113
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
1✔
114
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
115
                        }
×
116

117
                        original := latest.DeepCopy()
1✔
118
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
2✔
119
                                return s == targetFinalizer
1✔
120
                        })
1✔
121
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
1✔
122
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
123
                        }
×
124
                }
125

126
                return ctrl.Result{}, nil
1✔
127
        }
128

129
        // Set finalizer if not set
130
        if !slices.Contains(target.Finalizers, targetFinalizer) {
94✔
131
                latest := &solarv1alpha1.Target{}
18✔
132
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
18✔
133
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
134
                }
×
135

136
                original := latest.DeepCopy()
18✔
137
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
18✔
138
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
18✔
139
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
140
                }
×
141

142
                return ctrl.Result{}, nil
18✔
143
        }
144

145
        // Resolve render registry — supports cross-namespace via ReferenceGrant
146
        registryNamespace := target.Namespace
58✔
147
        if target.Spec.RenderRegistryNamespace != "" {
58✔
148
                registryNamespace = target.Spec.RenderRegistryNamespace
×
149
        }
×
150

151
        // If the registry lives in a different namespace, verify a ReferenceGrant permits it
152
        // before attempting to fetch the object.
153
        if registryNamespace != target.Namespace {
58✔
154
                granted, err := r.registryGranted(ctx, registryNamespace, target.Namespace)
×
155
                if err != nil {
×
156
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for Registry")
×
157
                }
×
158
                if !granted {
×
159
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotGranted",
×
160
                                "No ReferenceGrant allows access to Registry "+target.Spec.RenderRegistryRef.Name+" in namespace "+registryNamespace); condErr != nil {
×
161
                                return ctrl.Result{}, condErr
×
162
                        }
×
163

164
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
165
                }
166
        }
167

168
        registry := &solarv1alpha1.Registry{}
58✔
169
        if err := r.Get(ctx, client.ObjectKey{
58✔
170
                Name:      target.Spec.RenderRegistryRef.Name,
58✔
171
                Namespace: registryNamespace,
58✔
172
        }, registry); err != nil {
76✔
173
                if apierrors.IsNotFound(err) {
36✔
174
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
18✔
175
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
18✔
176
                                return ctrl.Result{}, condErr
×
177
                        }
×
178

179
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
18✔
180
                }
181

182
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
183
        }
184

185
        if registry.Spec.SolarSecretRef == nil {
42✔
186
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
187
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
188
                        return ctrl.Result{}, condErr
×
189
                }
×
190

191
                return ctrl.Result{}, nil
2✔
192
        }
193

194
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
38✔
195
                "Registry resolved: "+registry.Name); condErr != nil {
38✔
196
                return ctrl.Result{}, condErr
×
197
        }
×
198

199
        // Collect ReleaseBindings for this target
200
        bindingList := &solarv1alpha1.ReleaseBindingList{}
38✔
201
        if err := r.List(ctx, bindingList,
38✔
202
                client.InNamespace(target.Namespace),
38✔
203
                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
38✔
204
        ); err != nil {
38✔
205
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
206
        }
×
207

208
        if len(bindingList.Items) == 0 {
44✔
209
                log.V(1).Info("No ReleaseBindings found for target")
6✔
210
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoBindings",
6✔
211
                        "No ReleaseBindings found for this target"); condErr != nil {
6✔
212
                        return ctrl.Result{}, condErr
×
213
                }
×
214

215
                return ctrl.Result{}, nil
6✔
216
        }
217

218
        // For each bound release, ensure a per-release RenderTask exists
219
        var releases []releaseInfo
32✔
220

32✔
221
        pendingDeps := false
32✔
222

32✔
223
        for _, binding := range bindingList.Items {
82✔
224
                rel := &solarv1alpha1.Release{}
50✔
225
                if err := r.Get(ctx, client.ObjectKey{
50✔
226
                        Name:      binding.Spec.ReleaseRef.Name,
50✔
227
                        Namespace: target.Namespace,
50✔
228
                }, rel); err != nil {
50✔
229
                        if apierrors.IsNotFound(err) {
×
230
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
231
                                pendingDeps = true
×
232

×
233
                                continue
×
234
                        }
235

236
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
237
                }
238

239
                cv := &solarv1alpha1.ComponentVersion{}
50✔
240
                cvNamespace := target.Namespace
50✔
241
                if rel.Spec.ComponentVersionNamespace != "" {
50✔
242
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
243
                }
×
244

245
                if cvNamespace != target.Namespace {
50✔
246
                        granted := false
×
247
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
248
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
249
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
250
                        }
×
251
                        for i := range grantList.Items {
×
252
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
253
                                        granted = true
×
254
                                }
×
255
                        }
256
                        if !granted {
×
257
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
258
                                pendingDeps = true
×
259

×
260
                                continue
×
261
                        }
262
                }
263

264
                if err := r.Get(ctx, client.ObjectKey{
50✔
265
                        Name:      rel.Spec.ComponentVersionRef.Name,
50✔
266
                        Namespace: cvNamespace,
50✔
267
                }, cv); err != nil {
50✔
268
                        if apierrors.IsNotFound(err) {
×
269
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
270
                                pendingDeps = true
×
271

×
272
                                continue
×
273
                        }
274

275
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
276
                }
277

278
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
50✔
279
                releases = append(releases, releaseInfo{
50✔
280
                        bindingKey: binding.Namespace + "/" + binding.Name,
50✔
281
                        name:       rel.Name,
50✔
282
                        release:    rel,
50✔
283
                        cv:         cv,
50✔
284
                        rtName:     rtName,
50✔
285
                })
50✔
286
        }
287

288
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
289
        var skipped []string
32✔
290
        releases, skipped = resolveReleaseConflicts(releases)
32✔
291
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
32✔
NEW
292
                return ctrl.Result{}, condErr
×
NEW
293
        }
×
294

295
        if len(releases) == 0 && !pendingDeps {
32✔
NEW
296
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllBindingsFiltered",
×
NEW
297
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
NEW
298
                        return ctrl.Result{}, condErr
×
NEW
299
                }
×
300

NEW
301
                return ctrl.Result{}, nil
×
302
        }
303

304
        // Create per-release RenderTasks (one per target+release pair).
305
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
306
        allRendered := true
32✔
307

32✔
308
        for i, ri := range releases {
73✔
309
                rt := &solarv1alpha1.RenderTask{}
41✔
310
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
41✔
311

41✔
312
                if apierrors.IsNotFound(err) {
48✔
313
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
7✔
314
                        rt = &solarv1alpha1.RenderTask{
7✔
315
                                ObjectMeta: metav1.ObjectMeta{
7✔
316
                                        Name:      ri.rtName,
7✔
317
                                        Namespace: target.Namespace,
7✔
318
                                },
7✔
319
                                Spec: spec,
7✔
320
                        }
7✔
321

7✔
322
                        if err := r.Create(ctx, rt); err != nil {
7✔
323
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
324
                        }
×
325

326
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
7✔
327
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
328
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
7✔
329
                } else if err != nil {
34✔
330
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
331
                }
×
332

333
                // Check if release RenderTask is complete
334
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
41✔
335
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
336
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
337
                                return ctrl.Result{}, condErr
×
338
                        }
×
339

340
                        return ctrl.Result{}, nil
×
341
                }
342

343
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
61✔
344
                        releases[i].chartURL = rt.Status.ChartURL
20✔
345
                } else {
41✔
346
                        allRendered = false
21✔
347
                }
21✔
348
        }
349

350
        if pendingDeps {
32✔
351
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
352
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
353
                        return ctrl.Result{}, condErr
×
354
                }
×
355

356
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
357
        }
358

359
        if !allRendered {
53✔
360
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
21✔
361
                        "Waiting for release RenderTasks to complete"); condErr != nil {
21✔
362
                        return ctrl.Result{}, condErr
×
363
                }
×
364

365
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
21✔
366
        }
367

368
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
11✔
369
                "All releases rendered successfully"); condErr != nil {
11✔
370
                return ctrl.Result{}, condErr
×
371
        }
×
372

373
        // Determine if a new bootstrap render is needed by checking whether the
374
        // current bootstrapVersion's RenderTask still matches the desired release set.
375
        bootstrapVersion := target.Status.BootstrapVersion
11✔
376
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
11✔
377
        bootstrapRT := &solarv1alpha1.RenderTask{}
11✔
378
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
11✔
379

11✔
380
        needsNewBootstrap := false
11✔
381

11✔
382
        switch {
11✔
383
        case apierrors.IsNotFound(err):
1✔
384
                // No RenderTask for the current version yet — create one
1✔
385
                needsNewBootstrap = true
1✔
386
        case err != nil:
×
387
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
388
        default:
10✔
389
                // RenderTask exists — check if the desired bootstrap input changed
10✔
390
                // (release set, resolved refs/tags, or userdata)
10✔
391
                desiredInput, inputErr := buildBootstrapInput(target, releases)
10✔
392
                if inputErr != nil {
10✔
393
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
394
                }
×
395

396
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
10✔
397
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
11✔
398
                        bootstrapVersion++
1✔
399
                        needsNewBootstrap = true
1✔
400
                }
1✔
401
        }
402

403
        if needsNewBootstrap {
13✔
404
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
405
                if specErr != nil {
2✔
406
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
407
                }
×
408

409
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
410
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
411
                        ObjectMeta: metav1.ObjectMeta{
2✔
412
                                Name:      bootstrapRTName,
2✔
413
                                Namespace: target.Namespace,
2✔
414
                        },
2✔
415
                        Spec: spec,
2✔
416
                }
2✔
417

2✔
418
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
419
                        if !apierrors.IsAlreadyExists(err) {
×
420
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
421
                        }
×
422

423
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
424
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
425
                        }
×
426
                } else {
2✔
427
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
428
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
429
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
430
                }
2✔
431

432
                // Persist the new bootstrapVersion in status
433
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
434
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
435
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
436
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
437
                        }
×
438
                }
439
        }
440

441
        // Update target status from bootstrap RenderTask
442
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
11✔
443
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
444
                        "Bootstrap rendering failed"); condErr != nil {
×
445
                        return ctrl.Result{}, condErr
×
446
                }
×
447

448
                return ctrl.Result{}, nil
×
449
        }
450

451
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
16✔
452
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
5✔
453
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
5✔
454
                        return ctrl.Result{}, condErr
×
455
                }
×
456

457
                // Clean up stale RenderTasks owned by this target (old versions)
458
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
5✔
459
                for _, ri := range releases {
13✔
460
                        currentRTNames[ri.rtName] = struct{}{}
8✔
461
                }
8✔
462
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
5✔
463
                        log.Error(err, "failed to clean up stale RenderTasks")
×
464
                }
×
465

466
                return ctrl.Result{}, nil
5✔
467
        }
468

469
        // Still running
470
        return ctrl.Result{}, nil
6✔
471
}
472

473
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
133✔
474
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
133✔
475
                Type:               condType,
133✔
476
                Status:             status,
133✔
477
                ObservedGeneration: target.Generation,
133✔
478
                Reason:             reason,
133✔
479
                Message:            message,
133✔
480
        })
133✔
481
        if changed {
171✔
482
                if err := r.Status().Update(ctx, target); err != nil {
38✔
483
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
×
484
                }
×
485
        }
486

487
        return nil
133✔
488
}
489

490
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
32✔
491
        if len(skipped) == 0 {
55✔
492
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
23✔
493
        }
23✔
494

495
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
496
}
497

498
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
499
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
500
// It returns the accepted releases and a slice of human-readable skip messages.
501
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
39✔
502
        if len(releases) == 0 {
40✔
503
                return releases, nil
1✔
504
        }
1✔
505

506
        // Step A: uniqueName deduplication.
507
        // Releases with an empty uniqueName are never deduplicated.
508
        namedGroups := map[string][]releaseInfo{}
38✔
509
        var accepted []releaseInfo
38✔
510

38✔
511
        for _, ri := range releases {
97✔
512
                if ri.release.Spec.UniqueName == "" {
62✔
513
                        accepted = append(accepted, ri)
3✔
514
                        continue
3✔
515
                }
516

517
                namedGroups[ri.release.Spec.UniqueName] = append(namedGroups[ri.release.Spec.UniqueName], ri)
56✔
518
        }
519

520
        var skipped []string
38✔
521

38✔
522
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
38✔
523
        byPriority := func(a, b releaseInfo) bool {
59✔
524
                if a.release.Spec.Priority != b.release.Spec.Priority {
29✔
525
                        return a.release.Spec.Priority > b.release.Spec.Priority
8✔
526
                }
8✔
527

528
                return a.bindingKey < b.bindingKey
13✔
529
        }
530

531
        uniqueNames := make([]string, 0, len(namedGroups))
38✔
532
        for k := range namedGroups {
86✔
533
                uniqueNames = append(uniqueNames, k)
48✔
534
        }
48✔
535

536
        sort.Strings(uniqueNames)
38✔
537

38✔
538
        for _, uniqueName := range uniqueNames {
86✔
539
                group := namedGroups[uniqueName]
48✔
540
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
56✔
541

542
                accepted = append(accepted, group[0])
48✔
543

48✔
544
                for _, loser := range group[1:] {
56✔
545
                        skipped = append(skipped, fmt.Sprintf(
8✔
546
                                "binding %s skipped: uniqueName %q conflict, lower priority than %s",
8✔
547
                                loser.bindingKey, uniqueName, group[0].bindingKey,
8✔
548
                        ))
8✔
549
                }
8✔
550
        }
551

552
        // Step B: anti-affinity evaluation.
553
        // Walk in deterministic order (priority desc, bindingKey asc); accept each release only
554
        // if its AntiAffinity selector does not match any already-accepted release's labels.
555
        sort.Slice(accepted, func(i, j int) bool { return byPriority(accepted[i], accepted[j]) })
51✔
556

557
        resolved := make([]releaseInfo, 0, len(accepted))
38✔
558

38✔
559
        for _, ri := range accepted {
89✔
560
                if ri.release.Spec.AntiAffinity == nil {
96✔
561
                        resolved = append(resolved, ri)
45✔
562
                        continue
45✔
563
                }
564

565
                selector, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
6✔
566
                if err != nil {
7✔
567
                        skipped = append(skipped, fmt.Sprintf(
1✔
568
                                "binding %s skipped: invalid antiAffinity selector: %v",
1✔
569
                                ri.bindingKey, err,
1✔
570
                        ))
1✔
571

1✔
572
                        continue
1✔
573
                }
574

575
                conflict := ""
5✔
576
                for _, other := range resolved {
9✔
577
                        if selector.Matches(labels.Set(other.release.Labels)) {
8✔
578
                                conflict = other.bindingKey
4✔
579
                                break
4✔
580
                        }
581
                }
582

583
                if conflict != "" {
9✔
584
                        skipped = append(skipped, fmt.Sprintf(
4✔
585
                                "binding %s skipped: anti-affinity conflict with %s",
4✔
586
                                ri.bindingKey, conflict,
4✔
587
                        ))
4✔
588
                } else {
5✔
589
                        resolved = append(resolved, ri)
1✔
590
                }
1✔
591
        }
592

593
        return resolved, skipped
38✔
594
}
595

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

5✔
602
        rtList := &solarv1alpha1.RenderTaskList{}
5✔
603
        if err := r.List(ctx, rtList,
5✔
604
                client.InNamespace(target.Namespace),
5✔
605
                client.MatchingFields{indexOwnerKind: "Target"},
5✔
606
        ); err != nil {
5✔
607
                return err
×
608
        }
×
609

610
        for i := range rtList.Items {
19✔
611
                rt := &rtList.Items[i]
14✔
612
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
14✔
613
                        continue
×
614
                }
615

616
                if _, current := currentRTNames[rt.Name]; current {
27✔
617
                        continue
13✔
618
                }
619

620
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
621
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
622
                        return err
×
623
                }
×
624

625
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
626
                        "Deleted stale RenderTask %s", rt.Name)
1✔
627
        }
628

629
        return nil
5✔
630
}
631

632
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
633
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
634
        if err := r.List(ctx, rtList,
1✔
635
                client.InNamespace(target.Namespace),
1✔
636
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
637
        ); err != nil {
1✔
638
                return err
×
639
        }
×
640

641
        for i := range rtList.Items {
1✔
642
                rt := &rtList.Items[i]
×
643
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
644
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
645
                                return err
×
646
                        }
×
647
                }
648
        }
649

650
        return nil
1✔
651
}
652

653
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec {
7✔
654
        chartName := fmt.Sprintf("release-%s", rel.Name)
7✔
655
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
656
        tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration())
7✔
657

7✔
658
        var targetNamespace string
7✔
659
        if rel.Spec.TargetNamespace != nil {
14✔
660
                targetNamespace = *rel.Spec.TargetNamespace
7✔
661
        }
7✔
662

663
        return solarv1alpha1.RenderTaskSpec{
7✔
664
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
665
                        Type: solarv1alpha1.RendererConfigTypeRelease,
7✔
666
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
7✔
667
                                Chart: solarv1alpha1.ChartConfig{
7✔
668
                                        Name:        chartName,
7✔
669
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
7✔
670
                                        Version:     tag,
7✔
671
                                        AppVersion:  tag,
7✔
672
                                },
7✔
673
                                Input: solarv1alpha1.ReleaseInput{
7✔
674
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
7✔
675
                                        Resources:  cv.Spec.Resources,
7✔
676
                                        Entrypoint: cv.Spec.Entrypoint,
7✔
677
                                },
7✔
678
                                Values:          rel.Spec.Values,
7✔
679
                                TargetNamespace: targetNamespace,
7✔
680
                        },
7✔
681
                },
7✔
682
                Repository:     repo,
7✔
683
                Tag:            tag,
7✔
684
                BaseURL:        registry.Spec.Hostname,
7✔
685
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
686
                FailedJobTTL:   rel.Spec.FailedJobTTL,
7✔
687
                OwnerName:      target.Name,
7✔
688
                OwnerNamespace: target.Namespace,
7✔
689
                OwnerKind:      "Target",
7✔
690
        }
7✔
691
}
692

693
// buildBootstrapInput constructs the desired BootstrapInput from the current
694
// target and resolved releases. Used for both comparison and spec construction.
695
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
12✔
696
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
12✔
697

12✔
698
        for _, ri := range releases {
31✔
699
                ref, err := ociname.ParseReference(ri.chartURL)
19✔
700
                if err != nil {
19✔
701
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
702
                }
×
703

704
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
19✔
705
                if err != nil {
19✔
706
                        return solarv1alpha1.BootstrapInput{}, err
×
707
                }
×
708

709
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
19✔
710
                        Repository: strings.TrimPrefix(repo, "oci://"),
19✔
711
                        Tag:        ref.Identifier(),
19✔
712
                }
19✔
713
        }
714

715
        return solarv1alpha1.BootstrapInput{
12✔
716
                Releases: resolvedReleases,
12✔
717
                Userdata: target.Spec.Userdata,
12✔
718
        }, nil
12✔
719
}
720

721
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
2✔
722
        input, err := buildBootstrapInput(target, releases)
2✔
723
        if err != nil {
2✔
724
                return solarv1alpha1.RenderTaskSpec{}, err
×
725
        }
×
726

727
        releaseNames := make([]string, 0, len(releases))
2✔
728
        for _, ri := range releases {
5✔
729
                releaseNames = append(releaseNames, ri.name)
3✔
730
        }
3✔
731

732
        sort.Strings(releaseNames)
2✔
733

2✔
734
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
2✔
735
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
2✔
736
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
2✔
737

2✔
738
        return solarv1alpha1.RenderTaskSpec{
2✔
739
                RendererConfig: solarv1alpha1.RendererConfig{
2✔
740
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
2✔
741
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
2✔
742
                                Chart: solarv1alpha1.ChartConfig{
2✔
743
                                        Name:        chartName,
2✔
744
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
2✔
745
                                        Version:     tag,
2✔
746
                                        AppVersion:  tag,
2✔
747
                                },
2✔
748
                                Input: input,
2✔
749
                        },
2✔
750
                },
2✔
751
                Repository:     repo,
2✔
752
                Tag:            tag,
2✔
753
                BaseURL:        registry.Spec.Hostname,
2✔
754
                PushSecretRef:  registry.Spec.SolarSecretRef,
2✔
755
                OwnerName:      target.Name,
2✔
756
                OwnerNamespace: target.Namespace,
2✔
757
                OwnerKind:      "Target",
2✔
758
        }, nil
2✔
759
}
760

761
// SetupWithManager sets up the controller with the Manager.
762
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
763
        return ctrl.NewControllerManagedBy(mgr).
1✔
764
                For(&solarv1alpha1.Target{}).
1✔
765
                Watches(
1✔
766
                        &solarv1alpha1.ReleaseBinding{},
1✔
767
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
768
                ).
1✔
769
                Watches(
1✔
770
                        &solarv1alpha1.RenderTask{},
1✔
771
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
772
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
773
                ).
1✔
774
                Watches(
1✔
775
                        &solarv1alpha1.Registry{},
1✔
776
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
777
                ).
1✔
778
                Watches(
1✔
779
                        &solarv1alpha1.ReferenceGrant{},
1✔
780
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
781
                ).
1✔
782
                Watches(
1✔
783
                        &solarv1alpha1.Release{},
1✔
784
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
785
                ).
1✔
786
                Complete(r)
1✔
787
}
1✔
788

789
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
790
// fromNamespace to reference the named registry.
791
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
792
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
793
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
794
                return false, err
×
795
        }
×
796
        for i := range grantList.Items {
×
797
                grant := &grantList.Items[i]
×
798
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
799
                        return true, nil
×
800
                }
×
801
        }
802

803
        return false, nil
×
804
}
805

806
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
807
// fromNamespace to reference Registry resources in the grant's namespace.
808
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
809
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
810
}
×
811

812
// mapRegistryToTargets maps a Registry event to reconcile requests for all
813
// Targets that reference it — either in the same namespace or cross-namespace.
814
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
10✔
815
        reg, ok := obj.(*solarv1alpha1.Registry)
10✔
816
        if !ok {
10✔
817
                return nil
×
818
        }
×
819

820
        // Same-namespace targets
821
        targetList := &solarv1alpha1.TargetList{}
10✔
822
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
10✔
823
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
824

×
825
                return nil
×
826
        }
×
827

828
        var requests []reconcile.Request
10✔
829
        for _, t := range targetList.Items {
10✔
830
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
×
831
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
×
832
                        requests = append(requests, reconcile.Request{
×
833
                                NamespacedName: types.NamespacedName{
×
834
                                        Name:      t.Name,
×
835
                                        Namespace: t.Namespace,
×
836
                                },
×
837
                        })
×
838
                }
×
839
        }
840

841
        // Cross-namespace targets: find namespaces that have been granted access to
842
        // registries in reg.Namespace, then check their targets.
843
        grantList := &solarv1alpha1.ReferenceGrantList{}
10✔
844
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
10✔
845
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
846
                return requests
×
847
        }
×
848

849
        for i := range grantList.Items {
10✔
850
                grant := &grantList.Items[i]
×
851
                if !grantsRegistryResource(grant) {
×
852
                        continue
×
853
                }
854
                for _, from := range grant.Spec.From {
×
855
                        if from.Kind != "Target" || from.Group != solarGroup {
×
856
                                continue
×
857
                        }
858
                        crossTargets := &solarv1alpha1.TargetList{}
×
859
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
860
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
861
                                continue
×
862
                        }
863
                        for _, t := range crossTargets.Items {
×
864
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
865
                                        requests = append(requests, reconcile.Request{
×
866
                                                NamespacedName: types.NamespacedName{
×
867
                                                        Name:      t.Name,
×
868
                                                        Namespace: t.Namespace,
×
869
                                                },
×
870
                                        })
×
871
                                }
×
872
                        }
873
                }
874
        }
875

876
        return requests
10✔
877
}
878

879
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
880
// either because the grant controls Registry access (Target → Registry) or because
881
// it controls ComponentVersion access (Release → ComponentVersion, Releases live in
882
// the same namespace as their Targets).
883
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
8✔
884
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
8✔
885
        if !ok {
8✔
886
                return nil
×
887
        }
×
888

889
        var requests []reconcile.Request
8✔
890

8✔
891
        if grantsRegistryResource(grant) {
8✔
892
                for _, from := range grant.Spec.From {
×
893
                        if from.Kind != "Target" || from.Group != solarGroup {
×
894
                                continue
×
895
                        }
896
                        targets := &solarv1alpha1.TargetList{}
×
897
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
898
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
899
                                continue
×
900
                        }
901
                        for _, t := range targets.Items {
×
902
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
903
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
904
                                        requests = append(requests, reconcile.Request{
×
905
                                                NamespacedName: types.NamespacedName{
×
906
                                                        Name:      t.Name,
×
907
                                                        Namespace: t.Namespace,
×
908
                                                },
×
909
                                        })
×
910
                                }
×
911
                        }
912
                }
913
        }
914

915
        if grantsComponentVersionResource(grant) {
12✔
916
                for _, from := range grant.Spec.From {
8✔
917
                        if from.Kind != "Release" || from.Group != solarGroup {
4✔
918
                                continue
×
919
                        }
920
                        // Releases and Targets are co-located: list Targets in the Release's namespace.
921
                        targets := &solarv1alpha1.TargetList{}
4✔
922
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
923
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ComponentVersion grant mapping", "namespace", from.Namespace)
×
924
                                continue
×
925
                        }
926
                        for _, t := range targets.Items {
4✔
927
                                requests = append(requests, reconcile.Request{
×
928
                                        NamespacedName: types.NamespacedName{
×
929
                                                Name:      t.Name,
×
930
                                                Namespace: t.Namespace,
×
931
                                        },
×
932
                                })
×
933
                        }
×
934
                }
935
        }
936

937
        return requests
8✔
938
}
939

940
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
941
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
8✔
942
        for _, t := range grant.Spec.To {
16✔
943
                if t.Kind == "Registry" && t.Group == solarGroup {
8✔
944
                        return true
×
945
                }
×
946
        }
947

948
        return false
8✔
949
}
950

951
// mapReleaseToTargets maps a Release event to reconcile requests for all
952
// Targets that are bound to the release via ReleaseBindings.
953
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
56✔
954
        rel, ok := obj.(*solarv1alpha1.Release)
56✔
955
        if !ok {
56✔
956
                return nil
×
957
        }
×
958

959
        bindingList := &solarv1alpha1.ReleaseBindingList{}
56✔
960
        if err := r.List(ctx, bindingList,
56✔
961
                client.InNamespace(rel.Namespace),
56✔
962
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
56✔
963
        ); err != nil {
56✔
964
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
965

×
966
                return nil
×
967
        }
×
968

969
        seen := map[string]struct{}{}
56✔
970
        var requests []reconcile.Request
56✔
971

56✔
972
        for _, rb := range bindingList.Items {
56✔
973
                targetName := rb.Spec.TargetRef.Name
×
974
                if _, ok := seen[targetName]; ok {
×
975
                        continue
×
976
                }
977

978
                seen[targetName] = struct{}{}
×
979
                requests = append(requests, reconcile.Request{
×
980
                        NamespacedName: types.NamespacedName{
×
981
                                Name:      targetName,
×
982
                                Namespace: rb.Namespace,
×
983
                        },
×
984
                })
×
985
        }
986

987
        return requests
56✔
988
}
989

990
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
21✔
991
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
21✔
992
        if !ok || rb.Spec.TargetRef.Name == "" {
21✔
993
                return nil
×
994
        }
×
995

996
        return []reconcile.Request{
21✔
997
                {
21✔
998
                        NamespacedName: types.NamespacedName{
21✔
999
                                Name:      rb.Spec.TargetRef.Name,
21✔
1000
                                Namespace: rb.Namespace,
21✔
1001
                        },
21✔
1002
                },
21✔
1003
        }
21✔
1004
}
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