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

opendefensecloud / solution-arsenal / 27415403338

12 Jun 2026 12:23PM UTC coverage: 74.347% (+0.1%) from 74.245%
27415403338

push

github

web-flow
fix: fix empty resourceversion on multiple componentversions (#590)

## What
bugfix: multiple componentversions for a component led to empty
resourceversion which errored on component update, therefore not
creating the second..nth compoentverison for a component


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

* **Bug Fixes**
* Improved reliability when creating or updating components and their
versions so existing resources are correctly updated rather than causing
creation errors.

* **Tests**
* Added tests to verify conflict-free update behavior when target
resources already exist, ensuring specs are populated and no errors are
emitted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

6 of 10 new or added lines in 1 file covered. (60.0%)

4 existing lines in 2 files now uncovered.

2962 of 3984 relevant lines covered (74.35%)

35.54 hits per line

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

72.69
/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
        // uniqueName is the deduplication key computed by resolveReleaseConflicts:
52
        // Spec.UniqueName when set, otherwise the parent Component name from the CV.
53
        // It is guaranteed unique across all surviving releases and used as the
54
        // bootstrap map key to avoid collisions between same-named cross-namespace releases.
55
        uniqueName          string
56
        release             *solarv1alpha1.Release
57
        cv                  *solarv1alpha1.ComponentVersion
58
        rtName              string
59
        chartURL            string
60
        artifactName        string
61
        artifactBindingName string
62
}
63

64
type TargetReconciler struct {
65
        client.Client
66
        Scheme    *runtime.Scheme
67
        Recorder  events.EventRecorder
68
        APIReader client.Reader
69
        // WatchNamespace restricts reconciliation to this namespace.
70
        // Should be empty in production (watches all namespaces).
71
        // Intended for use in integration tests only.
72
        WatchNamespace string
73
        // RegistryBindingStrict enables strict registry binding mode.
74
        // When true, rendering fails if a resource's registry host has no
75
        // matching RegistryBinding. When false (default/relaxed), unmatched
76
        // hosts are treated as anonymous pull (no secretRef rendered).
77
        RegistryBindingStrict bool
78
}
79

80
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete
81
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch
82
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update
83
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get;list;watch
84
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch
85
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registrybindings,verbs=get;list;watch
86
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
87
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
88
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list;watch
89
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
90
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderartifacts,verbs=get;list;watch;create;update;patch
91
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderbindings,verbs=get;list;watch;create;update;patch;delete
92
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
93

94
// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release
95
// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask.
96
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
276✔
97
        log := ctrl.LoggerFrom(ctx)
276✔
98

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

276✔
101
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
368✔
102
                return ctrl.Result{}, nil
92✔
103
        }
92✔
104

105
        // Fetch target
106
        target := &solarv1alpha1.Target{}
184✔
107
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
188✔
108
                if apierrors.IsNotFound(err) {
8✔
109
                        return ctrl.Result{}, nil
4✔
110
                }
4✔
111

112
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
113
        }
114

115
        // Handle deletion
116
        if !target.DeletionTimestamp.IsZero() {
182✔
117
                log.V(1).Info("Target is being deleted")
2✔
118
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
2✔
119

2✔
120
                // Delete owned RenderTasks
2✔
121
                if err := r.deleteOwnedRenderTasks(ctx, target); err != nil {
2✔
122
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
123
                }
×
124

125
                // Delete owned RenderBindings so the GC controller can clean up orphaned RenderArtifacts.
126
                if err := r.deleteOwnedRenderBindings(ctx, target); err != nil {
2✔
127
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderBindings")
×
128
                }
×
129

130
                // Remove finalizer
131
                if slices.Contains(target.Finalizers, targetFinalizer) {
4✔
132
                        latest := &solarv1alpha1.Target{}
2✔
133
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
2✔
134
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
135
                        }
×
136

137
                        original := latest.DeepCopy()
2✔
138
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
4✔
139
                                return s == targetFinalizer
2✔
140
                        })
2✔
141
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
2✔
142
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
143
                        }
×
144
                }
145

146
                return ctrl.Result{}, nil
2✔
147
        }
148

149
        // Set finalizer if not set
150
        if !slices.Contains(target.Finalizers, targetFinalizer) {
212✔
151
                latest := &solarv1alpha1.Target{}
34✔
152
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
34✔
153
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
154
                }
×
155

156
                original := latest.DeepCopy()
34✔
157
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
34✔
158
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
34✔
UNCOV
159
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
UNCOV
160
                }
×
161

162
                return ctrl.Result{}, nil
34✔
163
        }
164

165
        // Resolve render registry — supports cross-namespace via ReferenceGrant
166
        registryNamespace := target.Namespace
144✔
167
        if target.Spec.RenderRegistryNamespace != "" {
144✔
168
                registryNamespace = target.Spec.RenderRegistryNamespace
×
169
        }
×
170

171
        // If the registry lives in a different namespace, verify a ReferenceGrant permits it
172
        // before attempting to fetch the object.
173
        if registryNamespace != target.Namespace {
144✔
174
                granted, err := r.registryGranted(ctx, registryNamespace, target.Namespace)
×
175
                if err != nil {
×
176
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for Registry")
×
177
                }
×
178
                if !granted {
×
179
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotGranted",
×
180
                                "No ReferenceGrant allows access to Registry "+target.Spec.RenderRegistryRef.Name+" in namespace "+registryNamespace); condErr != nil {
×
181
                                return ctrl.Result{}, condErr
×
182
                        }
×
183

184
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
185
                }
186
        }
187

188
        registry := &solarv1alpha1.Registry{}
144✔
189
        if err := r.Get(ctx, client.ObjectKey{
144✔
190
                Name:      target.Spec.RenderRegistryRef.Name,
144✔
191
                Namespace: registryNamespace,
144✔
192
        }, registry); err != nil {
163✔
193
                if apierrors.IsNotFound(err) {
38✔
194
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
19✔
195
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
19✔
196
                                return ctrl.Result{}, condErr
×
197
                        }
×
198

199
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
19✔
200
                }
201

202
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
203
        }
204

205
        if registry.Spec.SolarSecretRef == nil {
127✔
206
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
207
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
208
                        return ctrl.Result{}, condErr
×
209
                }
×
210

211
                return ctrl.Result{}, nil
2✔
212
        }
213

214
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
123✔
215
                "Registry resolved: "+registry.Name); condErr != nil {
123✔
216
                return ctrl.Result{}, condErr
×
217
        }
×
218

219
        // Build hostname→targetPullSecretName lookup from RegistryBindings for this target.
220
        pullSecretsByHost, err := r.buildPullSecretsLookup(ctx, target)
123✔
221
        if err != nil {
143✔
222
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "RegistryBindingConflict",
20✔
223
                        err.Error()); condErr != nil {
20✔
224
                        return ctrl.Result{}, condErr
×
225
                }
×
226

227
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to build pull secrets lookup from RegistryBindings")
20✔
228
        }
229

230
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
231
        allBindings := &solarv1alpha1.ReleaseBindingList{}
103✔
232
        if err := r.APIReader.List(ctx, allBindings, client.InNamespace(target.Namespace)); err != nil {
103✔
233
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
234
        }
×
235
        bindingList := &solarv1alpha1.ReleaseBindingList{}
103✔
236
        for _, rb := range allBindings.Items {
216✔
237
                if rb.Spec.TargetRef.Name == target.Name && rb.Spec.TargetNamespace == "" {
226✔
238
                        bindingList.Items = append(bindingList.Items, rb)
113✔
239
                }
113✔
240
        }
241

242
        // Collect cross-namespace ReleaseBindings authorized by ReferenceGrants in target's namespace.
243
        crossNsBindings, crossNsErr := r.collectCrossNamespaceReleaseBindings(ctx, target)
103✔
244
        if crossNsErr != nil {
103✔
245
                return ctrl.Result{}, errLogAndWrap(log, crossNsErr, "failed to collect cross-namespace ReleaseBindings")
×
246
        }
×
247
        bindingList.Items = append(bindingList.Items, crossNsBindings...)
103✔
248

103✔
249
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
103✔
250
        // RegistryBinding collection is wired into the rendering pipeline.
103✔
251

103✔
252
        if len(bindingList.Items) == 0 {
111✔
253
                log.V(1).Info("No ReleaseBindings found for target")
8✔
254
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
8✔
255
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
256
                        return ctrl.Result{}, condErr
×
257
                }
×
258

259
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
8✔
260
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
261
                        return ctrl.Result{}, condErr
×
262
                }
×
263

264
                // Clean up any stale RenderTasks and RenderBindings left from prior reconciles.
265
                if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
8✔
266
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderTasks after all bindings removed")
×
267
                }
×
268
                if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
8✔
269
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderBindings after all bindings removed")
×
270
                }
×
271

272
                return ctrl.Result{}, nil
8✔
273
        }
274

275
        // For each bound release, ensure a per-release RenderTask exists
276
        var releases []releaseInfo
95✔
277

95✔
278
        pendingDeps := false
95✔
279

95✔
280
        for _, binding := range bindingList.Items {
215✔
281
                rel := &solarv1alpha1.Release{}
120✔
282
                if err := r.Get(ctx, client.ObjectKey{
120✔
283
                        Name:      binding.Spec.ReleaseRef.Name,
120✔
284
                        Namespace: binding.Namespace,
120✔
285
                }, rel); err != nil {
120✔
286
                        if apierrors.IsNotFound(err) {
×
287
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
288
                                pendingDeps = true
×
289

×
290
                                continue
×
291
                        }
292

293
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
294
                }
295

296
                cv := &solarv1alpha1.ComponentVersion{}
120✔
297
                cvNamespace := rel.Namespace
120✔
298
                if rel.Spec.ComponentVersionNamespace != "" {
120✔
299
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
300
                }
×
301

302
                if cvNamespace != rel.Namespace {
120✔
303
                        granted := false
×
304
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
305
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
306
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
307
                        }
×
308
                        for i := range grantList.Items {
×
309
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
310
                                        granted = true
×
311
                                        break
×
312
                                }
313
                        }
314
                        if !granted {
×
315
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
316
                                pendingDeps = true
×
317

×
318
                                continue
×
319
                        }
320
                }
321

322
                if err := r.Get(ctx, client.ObjectKey{
120✔
323
                        Name:      rel.Spec.ComponentVersionRef.Name,
120✔
324
                        Namespace: cvNamespace,
120✔
325
                }, cv); err != nil {
120✔
326
                        if apierrors.IsNotFound(err) {
×
327
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
328
                                pendingDeps = true
×
329

×
330
                                continue
×
331
                        }
332

333
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
334
                }
335

336
                rtName := releaseRenderTaskName(rel.Namespace, rel.Name, target.Name, rel.GetGeneration())
120✔
337
                releases = append(releases, releaseInfo{
120✔
338
                        bindingKey: binding.Namespace + "/" + binding.Name,
120✔
339
                        name:       rel.Name,
120✔
340
                        release:    rel,
120✔
341
                        cv:         cv,
120✔
342
                        rtName:     rtName,
120✔
343
                })
120✔
344
        }
345

346
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
347
        var skipped []string
95✔
348
        releases, skipped = resolveReleaseConflicts(releases)
95✔
349
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
95✔
350
                return ctrl.Result{}, condErr
×
351
        }
×
352

353
        if len(releases) == 0 && !pendingDeps {
95✔
354
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
355
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
356
                        return ctrl.Result{}, condErr
×
357
                }
×
358

359
                return ctrl.Result{}, nil
×
360
        }
361

362
        // Create per-release RenderTasks (one per target+release pair).
363
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
364
        allRendered := true
95✔
365

95✔
366
        for i, ri := range releases {
206✔
367
                rt := &solarv1alpha1.RenderTask{}
111✔
368
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
111✔
369

111✔
370
                switch {
111✔
371
                case apierrors.IsNotFound(err):
29✔
372
                        spec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
29✔
373
                        if specErr != nil {
39✔
374
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
10✔
375
                                        specErr.Error()); condErr != nil {
10✔
376
                                        return ctrl.Result{}, condErr
×
377
                                }
×
378

379
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
10✔
380
                        }
381

382
                        rt = &solarv1alpha1.RenderTask{
19✔
383
                                ObjectMeta: metav1.ObjectMeta{
19✔
384
                                        Name:      ri.rtName,
19✔
385
                                        Namespace: target.Namespace,
19✔
386
                                },
19✔
387
                                Spec: spec,
19✔
388
                        }
19✔
389

19✔
390
                        if err := r.Create(ctx, rt); err != nil {
19✔
391
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
392
                        }
×
393

394
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
19✔
395
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
19✔
396
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
19✔
397
                case err != nil:
×
398
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
399
                default:
82✔
400
                        // RenderTask exists — check for spec drift (e.g. pull secrets
82✔
401
                        // changed after a RegistryBinding was created/updated).
82✔
402
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
82✔
403
                        if specErr != nil {
82✔
404
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
×
405
                                        specErr.Error()); condErr != nil {
×
406
                                        return ctrl.Result{}, condErr
×
407
                                }
×
408

409
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
410
                        }
411

412
                        if !apiequality.Semantic.DeepEqual(rt.Spec, desiredSpec) {
84✔
413
                                if err := r.Delete(ctx, rt); err != nil {
2✔
414
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete stale release RenderTask")
×
415
                                }
×
416

417
                                rt = &solarv1alpha1.RenderTask{
2✔
418
                                        ObjectMeta: metav1.ObjectMeta{
2✔
419
                                                Name:      ri.rtName,
2✔
420
                                                Namespace: target.Namespace,
2✔
421
                                        },
2✔
422
                                        Spec: desiredSpec,
2✔
423
                                }
2✔
424

2✔
425
                                if err := r.Create(ctx, rt); err != nil {
2✔
426
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to recreate release RenderTask")
×
427
                                }
×
428

429
                                log.V(1).Info("Recreated release RenderTask (spec drift)", "release", ri.name, "renderTask", ri.rtName)
2✔
430
                                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update",
2✔
431
                                        "Recreated release RenderTask %s for release %s (spec drift)", ri.rtName, ri.name)
2✔
432
                        }
433
                }
434

435
                // Check if release RenderTask is complete
436
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
101✔
437
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
438
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
439
                                return ctrl.Result{}, condErr
×
440
                        }
×
441

442
                        return ctrl.Result{}, nil
×
443
                }
444

445
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
143✔
446
                        releases[i].chartURL = rt.Status.ChartURL
42✔
447

42✔
448
                        // Ensure a RenderArtifact object exists for the pushed OCI artifact, and
42✔
449
                        // create a RenderBinding linking this Target to it.
42✔
450
                        aName := renderArtifactName(target.Namespace, rt.Spec.BaseURL, rt.Spec.Repository, rt.Spec.Tag)
42✔
451
                        bName := renderBindingName(aName, target.Name)
42✔
452
                        // Create the RenderBinding before the RenderArtifact to avoid a race
42✔
453
                        if err := r.ensureRenderBinding(ctx, target, aName, bName); err != nil {
42✔
454
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for release")
×
455
                        }
×
456
                        if err := r.ensureRenderArtifact(ctx, aName, rt, registry.Spec.Flavor, registryNamespace); err != nil {
42✔
457
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for release")
×
458
                        }
×
459
                        releases[i].artifactName = aName
42✔
460
                        releases[i].artifactBindingName = bName
42✔
461
                } else {
59✔
462
                        allRendered = false
59✔
463
                }
59✔
464
        }
465

466
        if pendingDeps {
85✔
467
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
468
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
469
                        return ctrl.Result{}, condErr
×
470
                }
×
471

472
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
473
        }
474

475
        if !allRendered {
141✔
476
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
56✔
477
                        "Waiting for release RenderTasks to complete"); condErr != nil {
59✔
478
                        return ctrl.Result{}, condErr
3✔
479
                }
3✔
480

481
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
53✔
482
        }
483

484
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
29✔
485
                "All releases rendered successfully"); condErr != nil {
29✔
486
                return ctrl.Result{}, condErr
×
487
        }
×
488

489
        // Determine if a new bootstrap render is needed by checking whether the
490
        // current bootstrapVersion's RenderTask still matches the desired release set.
491
        bootstrapVersion := target.Status.BootstrapVersion
29✔
492
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
29✔
493
        bootstrapRT := &solarv1alpha1.RenderTask{}
29✔
494
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
29✔
495

29✔
496
        needsNewBootstrap := false
29✔
497

29✔
498
        switch {
29✔
499
        case apierrors.IsNotFound(err):
5✔
500
                // No RenderTask for the current version yet — create one
5✔
501
                needsNewBootstrap = true
5✔
502
        case err != nil:
×
503
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
504
        default:
24✔
505
                // RenderTask exists — check if the desired bootstrap input changed
24✔
506
                // (release set, resolved refs/tags, or userdata)
24✔
507
                desiredInput, inputErr := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
24✔
508
                if inputErr != nil {
24✔
509
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
510
                }
×
511

512
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
24✔
513
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
27✔
514
                        bootstrapVersion++
3✔
515
                        needsNewBootstrap = true
3✔
516
                }
3✔
517
        }
518

519
        if needsNewBootstrap {
37✔
520
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
8✔
521
                if specErr != nil {
8✔
522
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
523
                }
×
524

525
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
8✔
526
                bootstrapRT = &solarv1alpha1.RenderTask{
8✔
527
                        ObjectMeta: metav1.ObjectMeta{
8✔
528
                                Name:      bootstrapRTName,
8✔
529
                                Namespace: target.Namespace,
8✔
530
                        },
8✔
531
                        Spec: spec,
8✔
532
                }
8✔
533

8✔
534
                if err := r.Create(ctx, bootstrapRT); err != nil {
9✔
535
                        if !apierrors.IsAlreadyExists(err) {
1✔
536
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
537
                        }
×
538

539
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
1✔
540
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
541
                        }
×
542
                } else {
7✔
543
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
7✔
544
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
545
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
7✔
546
                }
7✔
547

548
                // Persist the new bootstrapVersion in status
549
                if bootstrapVersion != target.Status.BootstrapVersion {
11✔
550
                        target.Status.BootstrapVersion = bootstrapVersion
3✔
551
                        if err := r.Status().Update(ctx, target); err != nil {
4✔
552
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
1✔
553
                        }
1✔
554
                }
555
        }
556

557
        // Update target status from bootstrap RenderTask
558
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
28✔
559
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
560
                        "Bootstrap rendering failed"); condErr != nil {
×
561
                        return ctrl.Result{}, condErr
×
562
                }
×
563

564
                return ctrl.Result{}, nil
×
565
        }
566

567
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
36✔
568
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
8✔
569
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
8✔
570
                        return ctrl.Result{}, condErr
×
571
                }
×
572

573
                // Ensure RenderArtifact + RenderBinding exist for the bootstrap chart.
574
                bootstrapArtifactName := renderArtifactName(target.Namespace, bootstrapRT.Spec.BaseURL, bootstrapRT.Spec.Repository, bootstrapRT.Spec.Tag)
8✔
575
                bootstrapBindingName := renderBindingName(bootstrapArtifactName, target.Name)
8✔
576
                // Create the RenderBinding before the RenderArtifact to avoid a race
8✔
577
                if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
8✔
578
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for bootstrap")
×
579
                }
×
580
                if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, registry.Spec.Flavor, registryNamespace); err != nil {
8✔
581
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for bootstrap")
×
582
                }
×
583

584
                // Clean up stale RenderTasks owned by this target (old versions)
585
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
8✔
586
                for _, ri := range releases {
20✔
587
                        currentRTNames[ri.rtName] = struct{}{}
12✔
588
                }
12✔
589
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
8✔
590
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
591
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
592
                        log.Error(err, "failed to clean up stale RenderTasks")
×
593
                }
×
594

595
                // Clean up stale RenderBindings owned by this target.
596
                currentBindingNames := map[string]struct{}{bootstrapBindingName: {}}
8✔
597
                for _, ri := range releases {
20✔
598
                        if ri.artifactBindingName != "" {
24✔
599
                                currentBindingNames[ri.artifactBindingName] = struct{}{}
12✔
600
                        }
12✔
601
                }
602
                if err := r.deleteStaleRenderBindings(ctx, target, currentBindingNames); err != nil {
8✔
603
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
604
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
605
                        log.Error(err, "failed to clean up stale RenderBindings")
×
606
                }
×
607

608
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
8✔
609
        }
610

611
        // Still running
612
        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
20✔
613
}
614

615
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
378✔
616
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
378✔
617
                Type:               condType,
378✔
618
                Status:             status,
378✔
619
                ObservedGeneration: target.Generation,
378✔
620
                Reason:             reason,
378✔
621
                Message:            message,
378✔
622
        })
378✔
623
        if changed {
473✔
624
                if err := r.Status().Update(ctx, target); err != nil {
98✔
625
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
3✔
626
                }
3✔
627
        }
628

629
        return nil
375✔
630
}
631

632
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
95✔
633
        if len(skipped) == 0 {
181✔
634
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
86✔
635
        }
86✔
636

637
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
638
}
639

640
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
641
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
642
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
643
// It returns the accepted releases and a slice of human-readable filter messages.
644
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
104✔
645
        if len(releases) == 0 {
105✔
646
                return releases, nil
1✔
647
        }
1✔
648

649
        // Step A: uniqueName deduplication.
650
        // When UniqueName is empty, fall back to the parent Component name from the CV.
651
        namedGroups := map[string][]releaseInfo{}
103✔
652

103✔
653
        for i, ri := range releases {
237✔
654
                uname := effectiveUniqueName(ri.release, ri.cv)
134✔
655
                releases[i].uniqueName = uname
134✔
656
                namedGroups[uname] = append(namedGroups[uname], releases[i])
134✔
657
        }
134✔
658

659
        var accepted []releaseInfo
103✔
660

103✔
661
        var skipped []string
103✔
662

103✔
663
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
103✔
664
        byPriority := func(a, b releaseInfo) bool {
134✔
665
                if a.release.Spec.Priority != b.release.Spec.Priority {
41✔
666
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
667
                }
10✔
668

669
                return a.bindingKey < b.bindingKey
21✔
670
        }
671

672
        uniqueNames := make([]string, 0, len(namedGroups))
103✔
673
        for k := range namedGroups {
228✔
674
                uniqueNames = append(uniqueNames, k)
125✔
675
        }
125✔
676

677
        sort.Strings(uniqueNames)
103✔
678

103✔
679
        for _, uniqueName := range uniqueNames {
228✔
680
                group := namedGroups[uniqueName]
125✔
681
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
134✔
682

683
                accepted = append(accepted, group[0])
125✔
684

125✔
685
                for _, loser := range group[1:] {
134✔
686
                        skipped = append(skipped, fmt.Sprintf(
9✔
687
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
688
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
689
                        ))
9✔
690
                }
9✔
691
        }
692

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

698
        resolved := make([]releaseInfo, 0, len(accepted))
103✔
699

103✔
700
        for _, ri := range accepted {
228✔
701
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
125✔
702
                var riSelector labels.Selector
125✔
703
                if ri.release.Spec.AntiAffinity != nil {
132✔
704
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
705
                        if err != nil {
8✔
706
                                skipped = append(skipped, fmt.Sprintf(
1✔
707
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
708
                                        ri.bindingKey, err,
1✔
709
                                ))
1✔
710

1✔
711
                                continue
1✔
712
                        }
713

714
                        riSelector = sel
6✔
715
                }
716

717
                // Check both directions: ri's anti-affinity against already-resolved labels,
718
                // and already-resolved anti-affinities against ri's labels.
719
                conflict := ""
124✔
720
                for _, other := range resolved {
146✔
721
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
26✔
722
                                conflict = other.bindingKey
4✔
723
                                break
4✔
724
                        }
725

726
                        if other.release.Spec.AntiAffinity != nil {
19✔
727
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
728
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
729
                                        conflict = other.bindingKey
1✔
730
                                        break
1✔
731
                                }
732
                        }
733
                }
734

735
                if conflict != "" {
129✔
736
                        skipped = append(skipped, fmt.Sprintf(
5✔
737
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
738
                                ri.bindingKey, conflict,
5✔
739
                        ))
5✔
740
                } else {
124✔
741
                        resolved = append(resolved, ri)
119✔
742
                }
119✔
743
        }
744

745
        return resolved, skipped
103✔
746
}
747

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

16✔
754
        rtList := &solarv1alpha1.RenderTaskList{}
16✔
755
        if err := r.List(ctx, rtList,
16✔
756
                client.InNamespace(target.Namespace),
16✔
757
                client.MatchingFields{indexOwnerKind: "Target"},
16✔
758
        ); err != nil {
16✔
759
                return err
×
760
        }
×
761

762
        for i := range rtList.Items {
39✔
763
                rt := &rtList.Items[i]
23✔
764
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
23✔
765
                        continue
×
766
                }
767

768
                if _, current := currentRTNames[rt.Name]; current {
43✔
769
                        continue
20✔
770
                }
771

772
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
3✔
773
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
3✔
774
                        return err
×
775
                }
×
776

777
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
3✔
778
                        "Deleted stale RenderTask %s", rt.Name)
3✔
779
        }
780

781
        return nil
16✔
782
}
783

784
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
785
        rtList := &solarv1alpha1.RenderTaskList{}
2✔
786
        if err := r.List(ctx, rtList,
2✔
787
                client.InNamespace(target.Namespace),
2✔
788
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
789
        ); err != nil {
2✔
790
                return err
×
791
        }
×
792

793
        for i := range rtList.Items {
4✔
794
                rt := &rtList.Items[i]
2✔
795
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
4✔
796
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
2✔
797
                                return err
×
798
                        }
×
799
                }
800
        }
801

802
        return nil
2✔
803
}
804

805
// deleteStaleRenderBindings removes RenderBindings owned by this target that are no
806
// longer needed (artifact is not in currentBindingNames).
807
func (r *TargetReconciler) deleteStaleRenderBindings(ctx context.Context, target *solarv1alpha1.Target, currentBindingNames map[string]struct{}) error {
16✔
808
        log := ctrl.LoggerFrom(ctx)
16✔
809

16✔
810
        bindingList := &solarv1alpha1.RenderBindingList{}
16✔
811
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
16✔
812
                return err
×
813
        }
×
814

815
        for i := range bindingList.Items {
39✔
816
                b := &bindingList.Items[i]
23✔
817
                if b.Spec.OwnerKind != "Target" || b.Spec.OwnerName != target.Name || b.Spec.OwnerNamespace != target.Namespace {
23✔
818
                        continue
×
819
                }
820

821
                if _, current := currentBindingNames[b.Name]; current {
43✔
822
                        continue
20✔
823
                }
824

825
                log.V(1).Info("Deleting stale RenderBinding", "renderBinding", b.Name)
3✔
826
                if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
3✔
827
                        return err
×
828
                }
×
829
        }
830

831
        return nil
16✔
832
}
833

834
// deleteOwnedRenderBindings removes all RenderBindings owned by this target.
835
// Called during Target deletion to trigger GC of any associated RenderArtifacts.
836
func (r *TargetReconciler) deleteOwnedRenderBindings(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
837
        bindingList := &solarv1alpha1.RenderBindingList{}
2✔
838
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
2✔
839
                return err
×
840
        }
×
841

842
        for i := range bindingList.Items {
3✔
843
                b := &bindingList.Items[i]
1✔
844
                if b.Spec.OwnerKind == "Target" && b.Spec.OwnerName == target.Name && b.Spec.OwnerNamespace == target.Namespace {
2✔
845
                        if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
1✔
846
                                return err
×
847
                        }
×
848
                }
849
        }
850

851
        return nil
2✔
852
}
853

854
// ensureRenderArtifact creates a RenderArtifact for the given RenderTask's OCI coordinates
855
// if one does not already exist. Idempotent: if it already exists (possibly created by
856
// another Target reconciling the same shared artifact), this is a no-op.
857
//
858
// pushSecretNamespace is passed explicitly because the secret may live in a different
859
// namespace than the RenderTask (e.g. a cluster-scoped secret namespace chosen by the
860
// operator). It must not be inferred from rt.Namespace.
861
func (r *TargetReconciler) ensureRenderArtifact(ctx context.Context, name string, rt *solarv1alpha1.RenderTask, flavor, pushSecretNamespace string) error {
50✔
862
        artifact := &solarv1alpha1.RenderArtifact{}
50✔
863
        if err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: rt.Namespace}, artifact); err == nil {
89✔
864
                if !artifact.DeletionTimestamp.IsZero() {
39✔
865
                        // The artifact is terminating (OCI cleanup in progress). Creating a binding
×
866
                        // against it would race with the finalizer. Requeue and wait for full deletion.
×
867
                        return fmt.Errorf("RenderArtifact %s/%s is terminating; requeuing", rt.Namespace, name)
×
868
                }
×
869

870
                return nil
39✔
871
        } else if !apierrors.IsNotFound(err) {
11✔
872
                return err
×
873
        }
×
874

875
        artifact = &solarv1alpha1.RenderArtifact{
11✔
876
                ObjectMeta: metav1.ObjectMeta{
11✔
877
                        Name:      name,
11✔
878
                        Namespace: rt.Namespace,
11✔
879
                },
11✔
880
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
881
                        BaseURL:             rt.Spec.BaseURL,
11✔
882
                        Repository:          rt.Spec.Repository,
11✔
883
                        Tag:                 rt.Spec.Tag,
11✔
884
                        RenderTaskRef:       rt.Name,
11✔
885
                        PushSecretRef:       rt.Spec.PushSecretRef,
11✔
886
                        PushSecretNamespace: pushSecretNamespace,
11✔
887
                        RegistryFlavor:      flavor,
11✔
888
                },
11✔
889
        }
11✔
890

11✔
891
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
892
                return err
×
893
        }
×
894

895
        return nil
11✔
896
}
897

898
// ensureRenderBinding creates a RenderBinding linking this Target to the named
899
// RenderArtifact if one does not already exist. Idempotent.
900
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string) error {
50✔
901
        binding := &solarv1alpha1.RenderBinding{}
50✔
902
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
89✔
903
                return nil
39✔
904
        } else if !apierrors.IsNotFound(err) {
50✔
905
                return err
×
906
        }
×
907

908
        binding = &solarv1alpha1.RenderBinding{
11✔
909
                ObjectMeta: metav1.ObjectMeta{
11✔
910
                        Name:      bindingName,
11✔
911
                        Namespace: target.Namespace,
11✔
912
                },
11✔
913
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
914
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
915
                        OwnerKind:         "Target",
11✔
916
                        OwnerName:         target.Name,
11✔
917
                        OwnerNamespace:    target.Namespace,
11✔
918
                },
11✔
919
        }
11✔
920

11✔
921
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
922
                return err
×
923
        }
×
924

925
        return nil
11✔
926
}
927

928
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target, pullSecretsByHost map[string]string) (solarv1alpha1.RenderTaskSpec, error) {
111✔
929
        chartName := fmt.Sprintf("release-%s", rel.Name)
111✔
930
        repo := fmt.Sprintf("%s/%s/%s", target.Namespace, rel.Namespace, chartName)
111✔
931

111✔
932
        var targetNamespace string
111✔
933
        if rel.Spec.TargetNamespace != nil {
222✔
934
                targetNamespace = *rel.Spec.TargetNamespace
111✔
935
        }
111✔
936

937
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
111✔
938
        if err != nil {
121✔
939
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
940
        }
10✔
941

942
        // Include a hash of pull-secret names in the tag so that charts whose
943
        // content differs only in secretRef get unique OCI tags. Without this,
944
        // the renderer's exists-check skips re-pushing after a spec-drift
945
        // recreation (e.g. RegistryBinding created after the first render).
946
        tag := fmt.Sprintf("v0.0.%d-%s", rel.GetGeneration(), pullSecretsTag(resolvedResources))
101✔
947

101✔
948
        return solarv1alpha1.RenderTaskSpec{
101✔
949
                RendererConfig: solarv1alpha1.RendererConfig{
101✔
950
                        Type: solarv1alpha1.RendererConfigTypeRelease,
101✔
951
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
101✔
952
                                Chart: solarv1alpha1.ChartConfig{
101✔
953
                                        Name:        chartName,
101✔
954
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
101✔
955
                                        Version:     tag,
101✔
956
                                        AppVersion:  tag,
101✔
957
                                },
101✔
958
                                Input: solarv1alpha1.ReleaseInput{
101✔
959
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
101✔
960
                                        Resources:  resolvedResources,
101✔
961
                                        Entrypoint: cv.Spec.Entrypoint,
101✔
962
                                },
101✔
963
                                Values:          rel.Spec.Values,
101✔
964
                                TargetNamespace: targetNamespace,
101✔
965
                        },
101✔
966
                },
101✔
967
                Repository:     repo,
101✔
968
                Tag:            tag,
101✔
969
                BaseURL:        registry.Spec.Hostname,
101✔
970
                PushSecretRef:  registry.Spec.SolarSecretRef,
101✔
971
                FailedJobTTL:   rel.Spec.FailedJobTTL,
101✔
972
                OwnerName:      target.Name,
101✔
973
                OwnerNamespace: target.Namespace,
101✔
974
                OwnerKind:      "Target",
101✔
975
        }, nil
101✔
976
}
977

978
// buildBootstrapInput constructs the desired BootstrapInput from the current
979
// target and resolved releases. Used for both comparison and spec construction.
980
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string) (solarv1alpha1.BootstrapInput, error) {
34✔
981
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
34✔
982

34✔
983
        for _, ri := range releases {
81✔
984
                if ri.uniqueName == "" {
48✔
985
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
986
                }
1✔
987

988
                ref, err := ociname.ParseReference(ri.chartURL)
46✔
989
                if err != nil {
46✔
990
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
991
                }
×
992

993
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
46✔
994
                if err != nil {
46✔
995
                        return solarv1alpha1.BootstrapInput{}, err
×
996
                }
×
997

998
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
46✔
999
                        Repository:     strings.TrimPrefix(repo, "oci://"),
46✔
1000
                        Tag:            ref.Identifier(),
46✔
1001
                        PullSecretName: renderRegistryPullSecret,
46✔
1002
                }
46✔
1003
        }
1004

1005
        return solarv1alpha1.BootstrapInput{
33✔
1006
                Releases: resolvedReleases,
33✔
1007
                Userdata: target.Spec.Userdata,
33✔
1008
        }, nil
33✔
1009
}
1010

1011
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
8✔
1012
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
8✔
1013
        if err != nil {
8✔
1014
                return solarv1alpha1.RenderTaskSpec{}, err
×
1015
        }
×
1016

1017
        releaseNames := make([]string, 0, len(releases))
8✔
1018
        for _, ri := range releases {
19✔
1019
                releaseNames = append(releaseNames, ri.name)
11✔
1020
        }
11✔
1021

1022
        sort.Strings(releaseNames)
8✔
1023

8✔
1024
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
8✔
1025
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
8✔
1026
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
8✔
1027

8✔
1028
        return solarv1alpha1.RenderTaskSpec{
8✔
1029
                RendererConfig: solarv1alpha1.RendererConfig{
8✔
1030
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
8✔
1031
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
8✔
1032
                                Chart: solarv1alpha1.ChartConfig{
8✔
1033
                                        Name:        chartName,
8✔
1034
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
8✔
1035
                                        Version:     tag,
8✔
1036
                                        AppVersion:  tag,
8✔
1037
                                },
8✔
1038
                                Input: input,
8✔
1039
                        },
8✔
1040
                },
8✔
1041
                Repository:     repo,
8✔
1042
                Tag:            tag,
8✔
1043
                BaseURL:        registry.Spec.Hostname,
8✔
1044
                PushSecretRef:  registry.Spec.SolarSecretRef,
8✔
1045
                OwnerName:      target.Name,
8✔
1046
                OwnerNamespace: target.Namespace,
8✔
1047
                OwnerKind:      "Target",
8✔
1048
        }, nil
8✔
1049
}
1050

1051
// SetupWithManager sets up the controller with the Manager.
1052
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
1053
        return ctrl.NewControllerManagedBy(mgr).
1✔
1054
                For(&solarv1alpha1.Target{}).
1✔
1055
                Watches(
1✔
1056
                        &solarv1alpha1.ReleaseBinding{},
1✔
1057
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
1058
                ).
1✔
1059
                Watches(
1✔
1060
                        &solarv1alpha1.RenderTask{},
1✔
1061
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
1062
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
1063
                ).
1✔
1064
                Watches(
1✔
1065
                        &solarv1alpha1.Registry{},
1✔
1066
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
1067
                ).
1✔
1068
                Watches(
1✔
1069
                        &solarv1alpha1.RegistryBinding{},
1✔
1070
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
1071
                ).
1✔
1072
                Watches(
1✔
1073
                        &solarv1alpha1.ReferenceGrant{},
1✔
1074
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
1075
                ).
1✔
1076
                Watches(
1✔
1077
                        &solarv1alpha1.Release{},
1✔
1078
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
1079
                ).
1✔
1080
                Complete(r)
1✔
1081
}
1✔
1082

1083
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
1084
// fromNamespace to reference the named registry.
1085
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
1086
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
1087
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
1088
                return false, err
×
1089
        }
×
1090
        for i := range grantList.Items {
×
1091
                grant := &grantList.Items[i]
×
1092
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
1093
                        return true, nil
×
1094
                }
×
1095
        }
1096

1097
        return false, nil
×
1098
}
1099

1100
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
1101
// fromNamespace to reference Registry resources in the grant's namespace.
1102
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
1103
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
1104
}
×
1105

1106
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1107
// Targets that reference it — either in the same namespace or cross-namespace.
1108
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
31✔
1109
        reg, ok := obj.(*solarv1alpha1.Registry)
31✔
1110
        if !ok {
31✔
1111
                return nil
×
1112
        }
×
1113

1114
        // Same-namespace targets
1115
        targetList := &solarv1alpha1.TargetList{}
31✔
1116
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
31✔
1117
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
1118

×
1119
                return nil
×
1120
        }
×
1121

1122
        var requests []reconcile.Request
31✔
1123
        for _, t := range targetList.Items {
32✔
1124
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
1✔
1125
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
1✔
1126
                        requests = append(requests, reconcile.Request{
×
1127
                                NamespacedName: types.NamespacedName{
×
1128
                                        Name:      t.Name,
×
1129
                                        Namespace: t.Namespace,
×
1130
                                },
×
1131
                        })
×
1132
                }
×
1133
        }
1134

1135
        // Cross-namespace targets: find namespaces that have been granted access to
1136
        // registries in reg.Namespace, then check their targets.
1137
        grantList := &solarv1alpha1.ReferenceGrantList{}
31✔
1138
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
31✔
1139
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
1140
                return requests
×
1141
        }
×
1142

1143
        for i := range grantList.Items {
31✔
1144
                grant := &grantList.Items[i]
×
1145
                if !grantsRegistryResource(grant) {
×
1146
                        continue
×
1147
                }
1148
                for _, from := range grant.Spec.From {
×
1149
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1150
                                continue
×
1151
                        }
1152
                        crossTargets := &solarv1alpha1.TargetList{}
×
1153
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
1154
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
1155
                                continue
×
1156
                        }
1157
                        for _, t := range crossTargets.Items {
×
1158
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
1159
                                        requests = append(requests, reconcile.Request{
×
1160
                                                NamespacedName: types.NamespacedName{
×
1161
                                                        Name:      t.Name,
×
1162
                                                        Namespace: t.Namespace,
×
1163
                                                },
×
1164
                                        })
×
1165
                                }
×
1166
                        }
1167
                }
1168
        }
1169

1170
        return requests
31✔
1171
}
1172

1173
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
1174
// each bound Registry, and returns a map from registry hostname to
1175
// targetPullSecretName. Registries without a targetPullSecretName are included
1176
// with an empty string (anonymous pull).
1177
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
123✔
1178
        rbList := &solarv1alpha1.RegistryBindingList{}
123✔
1179
        if err := r.List(ctx, rbList,
123✔
1180
                client.InNamespace(target.Namespace),
123✔
1181
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
123✔
1182
        ); err != nil {
123✔
1183
                return nil, err
×
1184
        }
×
1185

1186
        type hostEntry struct {
123✔
1187
                pullSecret  string
123✔
1188
                bindingName string
123✔
1189
        }
123✔
1190

123✔
1191
        lookup := make(map[string]hostEntry, len(rbList.Items))
123✔
1192

123✔
1193
        for _, rb := range rbList.Items {
161✔
1194
                reg := &solarv1alpha1.Registry{}
38✔
1195
                if err := r.Get(ctx, client.ObjectKey{
38✔
1196
                        Name:      rb.Spec.RegistryRef.Name,
38✔
1197
                        Namespace: rb.Namespace,
38✔
1198
                }, reg); err != nil {
48✔
1199
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1200
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1201
                }
10✔
1202

1203
                host := strings.ToLower(reg.Spec.Hostname)
28✔
1204
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
38✔
1205
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
10✔
1206
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1207
                }
10✔
1208

1209
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
18✔
1210
        }
1211

1212
        result := make(map[string]string, len(lookup))
103✔
1213
        for host, entry := range lookup {
111✔
1214
                result[host] = entry.pullSecret
8✔
1215
        }
8✔
1216

1217
        return result, nil
103✔
1218
}
1219

1220
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1221
// for the referenced Target.
1222
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
7✔
1223
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
7✔
1224
        if !ok {
7✔
1225
                return nil
×
1226
        }
×
1227

1228
        if rb.Spec.TargetRef.Name == "" {
7✔
1229
                return nil
×
1230
        }
×
1231

1232
        return []reconcile.Request{
7✔
1233
                {
7✔
1234
                        NamespacedName: types.NamespacedName{
7✔
1235
                                Name:      rb.Spec.TargetRef.Name,
7✔
1236
                                Namespace: rb.Namespace,
7✔
1237
                        },
7✔
1238
                },
7✔
1239
        }
7✔
1240
}
1241

1242
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1243
// either because the grant controls Registry access (Target → Registry) or because
1244
// it controls ComponentVersion access (Release → ComponentVersion).
1245
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
13✔
1246
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
13✔
1247
        if !ok {
13✔
1248
                return nil
×
1249
        }
×
1250

1251
        var requests []reconcile.Request
13✔
1252

13✔
1253
        if grantsRegistryResource(grant) {
13✔
1254
                for _, from := range grant.Spec.From {
×
1255
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1256
                                continue
×
1257
                        }
1258
                        targets := &solarv1alpha1.TargetList{}
×
1259
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
1260
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1261
                                continue
×
1262
                        }
1263
                        for _, t := range targets.Items {
×
1264
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
1265
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
1266
                                        requests = append(requests, reconcile.Request{
×
1267
                                                NamespacedName: types.NamespacedName{
×
1268
                                                        Name:      t.Name,
×
1269
                                                        Namespace: t.Namespace,
×
1270
                                                },
×
1271
                                        })
×
1272
                                }
×
1273
                        }
1274
                }
1275
        }
1276

1277
        if grantsComponentVersionResource(grant) {
18✔
1278
                seen := map[string]struct{}{}
5✔
1279
                for _, from := range grant.Spec.From {
10✔
1280
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
1281
                                continue
×
1282
                        }
1283
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
1284
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
1285
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1286
                                continue
×
1287
                        }
1288
                        for _, rb := range bindings.Items {
6✔
1289
                                if rb.Spec.TargetRef.Name == "" {
1✔
1290
                                        continue
×
1291
                                }
1292
                                targetNs := rb.Namespace
1✔
1293
                                if rb.Spec.TargetNamespace != "" {
2✔
1294
                                        targetNs = rb.Spec.TargetNamespace
1✔
1295
                                }
1✔
1296
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1297
                                if _, ok := seen[key]; ok {
1✔
1298
                                        continue
×
1299
                                }
1300
                                seen[key] = struct{}{}
1✔
1301
                                requests = append(requests, reconcile.Request{
1✔
1302
                                        NamespacedName: types.NamespacedName{
1✔
1303
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1304
                                                Namespace: targetNs,
1✔
1305
                                        },
1✔
1306
                                })
1✔
1307
                        }
1308
                }
1309
        }
1310

1311
        if grantsReleaseBindingToTargetResource(grant) {
17✔
1312
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1313
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1314
                // the new or removed cross-namespace ReleaseBindings.
4✔
1315
                targets := &solarv1alpha1.TargetList{}
4✔
1316
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1317
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1318
                } else {
4✔
1319
                        for _, t := range targets.Items {
8✔
1320
                                requests = append(requests, reconcile.Request{
4✔
1321
                                        NamespacedName: types.NamespacedName{
4✔
1322
                                                Name:      t.Name,
4✔
1323
                                                Namespace: t.Namespace,
4✔
1324
                                        },
4✔
1325
                                })
4✔
1326
                        }
4✔
1327
                }
1328
        }
1329

1330
        return requests
13✔
1331
}
1332

1333
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1334
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
13✔
1335
        for _, t := range grant.Spec.To {
26✔
1336
                if t.Kind == "Registry" && t.Group == solarGroup {
13✔
1337
                        return true
×
1338
                }
×
1339
        }
1340

1341
        return false
13✔
1342
}
1343

1344
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1345
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1346
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
21✔
1347
        hasReleaseBindingFrom := false
21✔
1348
        for _, f := range grant.Spec.From {
42✔
1349
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
33✔
1350
                        hasReleaseBindingFrom = true
12✔
1351
                        break
12✔
1352
                }
1353
        }
1354
        if !hasReleaseBindingFrom {
30✔
1355
                return false
9✔
1356
        }
9✔
1357
        for _, t := range grant.Spec.To {
24✔
1358
                if t.Kind == "Target" && t.Group == solarGroup {
24✔
1359
                        return true
12✔
1360
                }
12✔
1361
        }
1362

1363
        return false
×
1364
}
1365

1366
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1367
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1368
// a ReferenceGrant in target's namespace.
1369
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
103✔
1370
        grantList := &solarv1alpha1.ReferenceGrantList{}
103✔
1371
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
103✔
1372
                return nil, err
×
1373
        }
×
1374

1375
        seen := make(map[string]struct{})
103✔
1376
        var result []solarv1alpha1.ReleaseBinding
103✔
1377
        for i := range grantList.Items {
111✔
1378
                grant := &grantList.Items[i]
8✔
1379
                if !grantsReleaseBindingToTargetResource(grant) {
8✔
1380
                        continue
×
1381
                }
1382
                for _, from := range grant.Spec.From {
16✔
1383
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
8✔
1384
                                continue
×
1385
                        }
1386
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
8✔
1387
                        if err := r.List(ctx, crossBindings,
8✔
1388
                                client.InNamespace(from.Namespace),
8✔
1389
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
8✔
1390
                        ); err != nil {
8✔
1391
                                return nil, err
×
1392
                        }
×
1393
                        for _, rb := range crossBindings.Items {
16✔
1394
                                if rb.Spec.TargetNamespace != target.Namespace {
8✔
1395
                                        continue
×
1396
                                }
1397
                                key := rb.Namespace + "/" + rb.Name
8✔
1398
                                if _, exists := seen[key]; exists {
9✔
1399
                                        continue
1✔
1400
                                }
1401
                                seen[key] = struct{}{}
7✔
1402
                                result = append(result, rb)
7✔
1403
                        }
1404
                }
1405
        }
1406

1407
        return result, nil
103✔
1408
}
1409

1410
// mapReleaseToTargets maps a Release event to reconcile requests for all
1411
// Targets that are bound to the release via ReleaseBindings.
1412
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
105✔
1413
        rel, ok := obj.(*solarv1alpha1.Release)
105✔
1414
        if !ok {
105✔
1415
                return nil
×
1416
        }
×
1417

1418
        bindingList := &solarv1alpha1.ReleaseBindingList{}
105✔
1419
        if err := r.List(ctx, bindingList,
105✔
1420
                client.InNamespace(rel.Namespace),
105✔
1421
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
105✔
1422
        ); err != nil {
105✔
1423
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1424

×
1425
                return nil
×
1426
        }
×
1427

1428
        seen := map[string]struct{}{}
105✔
1429
        var requests []reconcile.Request
105✔
1430

105✔
1431
        for _, rb := range bindingList.Items {
107✔
1432
                targetNs := rb.Namespace
2✔
1433
                if rb.Spec.TargetNamespace != "" {
2✔
1434
                        targetNs = rb.Spec.TargetNamespace
×
1435
                }
×
1436

1437
                key := targetNs + "/" + rb.Spec.TargetRef.Name
2✔
1438
                if _, ok := seen[key]; ok {
2✔
1439
                        continue
×
1440
                }
1441

1442
                seen[key] = struct{}{}
2✔
1443
                requests = append(requests, reconcile.Request{
2✔
1444
                        NamespacedName: types.NamespacedName{
2✔
1445
                                Name:      rb.Spec.TargetRef.Name,
2✔
1446
                                Namespace: targetNs,
2✔
1447
                        },
2✔
1448
                })
2✔
1449
        }
1450

1451
        return requests
105✔
1452
}
1453

1454
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
39✔
1455
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
39✔
1456
        if !ok || rb.Spec.TargetRef.Name == "" {
39✔
1457
                return nil
×
1458
        }
×
1459

1460
        targetNs := rb.Namespace
39✔
1461
        if rb.Spec.TargetNamespace != "" {
48✔
1462
                targetNs = rb.Spec.TargetNamespace
9✔
1463
        }
9✔
1464

1465
        return []reconcile.Request{
39✔
1466
                {
39✔
1467
                        NamespacedName: types.NamespacedName{
39✔
1468
                                Name:      rb.Spec.TargetRef.Name,
39✔
1469
                                Namespace: targetNs,
39✔
1470
                        },
39✔
1471
                },
39✔
1472
        }
39✔
1473
}
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