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

opendefensecloud / solution-arsenal / 26496082009

27 May 2026 07:00AM UTC coverage: 72.275%. First build
26496082009

Pull #541

github

web-flow
Merge 73098c57c into ce1ad469b
Pull Request #541: feat: finish ReferenceGrants implementation (#490)

88 of 114 new or added lines in 2 files covered. (77.19%)

2440 of 3376 relevant lines covered (72.27%)

33.71 hits per line

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

69.17
/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=events.k8s.io,resources=events,verbs=create;patch
77

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

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

159✔
85
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
220✔
86
                return ctrl.Result{}, nil
61✔
87
        }
61✔
88

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

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

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

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

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

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

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

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

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

141
                return ctrl.Result{}, nil
22✔
142
        }
143

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

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

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

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

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

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

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

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

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

198
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
199
        // Filter on targetNamespace="" to exclude cross-namespace bindings (targetNamespace set) that share the
200
        // target name but point to a target in a different namespace.
201
        bindingList := &solarv1alpha1.ReleaseBindingList{}
50✔
202
        if err := r.List(ctx, bindingList,
50✔
203
                client.InNamespace(target.Namespace),
50✔
204
                client.MatchingFields{
50✔
205
                        indexReleaseBindingTargetName:      target.Name,
50✔
206
                        indexReleaseBindingTargetNamespace: "",
50✔
207
                },
50✔
208
        ); err != nil {
50✔
209
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
210
        }
×
211

212
        // Collect cross-namespace ReleaseBindings authorized by ReferenceGrants in target's namespace.
213
        crossNsBindings, crossNsErr := r.collectCrossNamespaceReleaseBindings(ctx, target)
50✔
214
        if crossNsErr != nil {
50✔
NEW
215
                return ctrl.Result{}, errLogAndWrap(log, crossNsErr, "failed to collect cross-namespace ReleaseBindings")
×
NEW
216
        }
×
217
        bindingList.Items = append(bindingList.Items, crossNsBindings...)
50✔
218

50✔
219
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
50✔
220
        // RegistryBinding collection is wired into the rendering pipeline.
50✔
221

50✔
222
        if len(bindingList.Items) == 0 {
60✔
223
                log.V(1).Info("No ReleaseBindings found for target")
10✔
224
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
10✔
225
                        "No ReleaseBindings found for this target"); condErr != nil {
10✔
226
                        return ctrl.Result{}, condErr
×
227
                }
×
228

229
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
10✔
230
                        "No ReleaseBindings found for this target"); condErr != nil {
10✔
231
                        return ctrl.Result{}, condErr
×
232
                }
×
233

234
                return ctrl.Result{}, nil
10✔
235
        }
236

237
        // For each bound release, ensure a per-release RenderTask exists
238
        var releases []releaseInfo
40✔
239

40✔
240
        pendingDeps := false
40✔
241

40✔
242
        for _, binding := range bindingList.Items {
99✔
243
                rel := &solarv1alpha1.Release{}
59✔
244
                if err := r.Get(ctx, client.ObjectKey{
59✔
245
                        Name:      binding.Spec.ReleaseRef.Name,
59✔
246
                        Namespace: binding.Namespace,
59✔
247
                }, rel); err != nil {
59✔
248
                        if apierrors.IsNotFound(err) {
×
249
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
250
                                pendingDeps = true
×
251

×
252
                                continue
×
253
                        }
254

255
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
256
                }
257

258
                cv := &solarv1alpha1.ComponentVersion{}
59✔
259
                cvNamespace := rel.Namespace
59✔
260
                if rel.Spec.ComponentVersionNamespace != "" {
59✔
261
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
262
                }
×
263

264
                if cvNamespace != rel.Namespace {
59✔
265
                        granted := false
×
266
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
267
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
268
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
269
                        }
×
270
                        for i := range grantList.Items {
×
271
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
272
                                        granted = true
×
NEW
273
                                        break
×
274
                                }
275
                        }
276
                        if !granted {
×
277
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
278
                                pendingDeps = true
×
279

×
280
                                continue
×
281
                        }
282
                }
283

284
                if err := r.Get(ctx, client.ObjectKey{
59✔
285
                        Name:      rel.Spec.ComponentVersionRef.Name,
59✔
286
                        Namespace: cvNamespace,
59✔
287
                }, cv); err != nil {
59✔
288
                        if apierrors.IsNotFound(err) {
×
289
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
290
                                pendingDeps = true
×
291

×
292
                                continue
×
293
                        }
294

295
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
296
                }
297

298
                rtName := releaseRenderTaskName(rel.Namespace, rel.Name, target.Name, rel.GetGeneration())
59✔
299
                releases = append(releases, releaseInfo{
59✔
300
                        bindingKey: binding.Namespace + "/" + binding.Name,
59✔
301
                        name:       rel.Name,
59✔
302
                        release:    rel,
59✔
303
                        cv:         cv,
59✔
304
                        rtName:     rtName,
59✔
305
                })
59✔
306
        }
307

308
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
309
        var skipped []string
40✔
310
        releases, skipped = resolveReleaseConflicts(releases)
40✔
311
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
40✔
312
                return ctrl.Result{}, condErr
×
313
        }
×
314

315
        if len(releases) == 0 && !pendingDeps {
40✔
316
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
317
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
318
                        return ctrl.Result{}, condErr
×
319
                }
×
320

321
                return ctrl.Result{}, nil
×
322
        }
323

324
        // Create per-release RenderTasks (one per target+release pair).
325
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
326
        allRendered := true
40✔
327

40✔
328
        for i, ri := range releases {
90✔
329
                rt := &solarv1alpha1.RenderTask{}
50✔
330
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
50✔
331

50✔
332
                if apierrors.IsNotFound(err) {
60✔
333
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
10✔
334
                        rt = &solarv1alpha1.RenderTask{
10✔
335
                                ObjectMeta: metav1.ObjectMeta{
10✔
336
                                        Name:      ri.rtName,
10✔
337
                                        Namespace: target.Namespace,
10✔
338
                                },
10✔
339
                                Spec: spec,
10✔
340
                        }
10✔
341

10✔
342
                        if err := r.Create(ctx, rt); err != nil {
10✔
343
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
344
                        }
×
345

346
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
10✔
347
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
10✔
348
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
10✔
349
                } else if err != nil {
40✔
350
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
351
                }
×
352

353
                // Check if release RenderTask is complete
354
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
50✔
355
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
356
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
357
                                return ctrl.Result{}, condErr
×
358
                        }
×
359

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

363
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
72✔
364
                        releases[i].chartURL = rt.Status.ChartURL
22✔
365
                } else {
50✔
366
                        allRendered = false
28✔
367
                }
28✔
368
        }
369

370
        if pendingDeps {
40✔
371
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
372
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
373
                        return ctrl.Result{}, condErr
×
374
                }
×
375

376
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
377
        }
378

379
        if !allRendered {
68✔
380
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
28✔
381
                        "Waiting for release RenderTasks to complete"); condErr != nil {
28✔
382
                        return ctrl.Result{}, condErr
×
383
                }
×
384

385
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
28✔
386
        }
387

388
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
12✔
389
                "All releases rendered successfully"); condErr != nil {
12✔
390
                return ctrl.Result{}, condErr
×
391
        }
×
392

393
        // Determine if a new bootstrap render is needed by checking whether the
394
        // current bootstrapVersion's RenderTask still matches the desired release set.
395
        bootstrapVersion := target.Status.BootstrapVersion
12✔
396
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
12✔
397
        bootstrapRT := &solarv1alpha1.RenderTask{}
12✔
398
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
12✔
399

12✔
400
        needsNewBootstrap := false
12✔
401

12✔
402
        switch {
12✔
403
        case apierrors.IsNotFound(err):
1✔
404
                // No RenderTask for the current version yet — create one
1✔
405
                needsNewBootstrap = true
1✔
406
        case err != nil:
×
407
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
408
        default:
11✔
409
                // RenderTask exists — check if the desired bootstrap input changed
11✔
410
                // (release set, resolved refs/tags, or userdata)
11✔
411
                desiredInput, inputErr := buildBootstrapInput(target, releases)
11✔
412
                if inputErr != nil {
11✔
413
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
414
                }
×
415

416
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
11✔
417
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
12✔
418
                        bootstrapVersion++
1✔
419
                        needsNewBootstrap = true
1✔
420
                }
1✔
421
        }
422

423
        if needsNewBootstrap {
14✔
424
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
425
                if specErr != nil {
2✔
426
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
427
                }
×
428

429
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
430
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
431
                        ObjectMeta: metav1.ObjectMeta{
2✔
432
                                Name:      bootstrapRTName,
2✔
433
                                Namespace: target.Namespace,
2✔
434
                        },
2✔
435
                        Spec: spec,
2✔
436
                }
2✔
437

2✔
438
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
439
                        if !apierrors.IsAlreadyExists(err) {
×
440
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
441
                        }
×
442

443
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
444
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
445
                        }
×
446
                } else {
2✔
447
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
448
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
449
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
450
                }
2✔
451

452
                // Persist the new bootstrapVersion in status
453
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
454
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
455
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
456
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
457
                        }
×
458
                }
459
        }
460

461
        // Update target status from bootstrap RenderTask
462
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
12✔
463
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
464
                        "Bootstrap rendering failed"); condErr != nil {
×
465
                        return ctrl.Result{}, condErr
×
466
                }
×
467

468
                return ctrl.Result{}, nil
×
469
        }
470

471
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
17✔
472
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
5✔
473
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
5✔
474
                        return ctrl.Result{}, condErr
×
475
                }
×
476

477
                // Clean up stale RenderTasks owned by this target (old versions)
478
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
5✔
479
                for _, ri := range releases {
13✔
480
                        currentRTNames[ri.rtName] = struct{}{}
8✔
481
                }
8✔
482
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
5✔
483
                        log.Error(err, "failed to clean up stale RenderTasks")
×
484
                }
×
485

486
                return ctrl.Result{}, nil
5✔
487
        }
488

489
        // Still running
490
        return ctrl.Result{}, nil
7✔
491
}
492

493
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
176✔
494
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
176✔
495
                Type:               condType,
176✔
496
                Status:             status,
176✔
497
                ObservedGeneration: target.Generation,
176✔
498
                Reason:             reason,
176✔
499
                Message:            message,
176✔
500
        })
176✔
501
        if changed {
233✔
502
                if err := r.Status().Update(ctx, target); err != nil {
57✔
503
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
×
504
                }
×
505
        }
506

507
        return nil
176✔
508
}
509

510
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
40✔
511
        if len(skipped) == 0 {
71✔
512
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
31✔
513
        }
31✔
514

515
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
516
}
517

518
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
519
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
520
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
521
// It returns the accepted releases and a slice of human-readable filter messages.
522
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
48✔
523
        if len(releases) == 0 {
49✔
524
                return releases, nil
1✔
525
        }
1✔
526

527
        // Step A: uniqueName deduplication.
528
        // When UniqueName is empty, fall back to the parent Component name from the CV.
529
        namedGroups := map[string][]releaseInfo{}
47✔
530

47✔
531
        for _, ri := range releases {
118✔
532
                uniqueName := ri.release.Spec.UniqueName
71✔
533
                if uniqueName == "" {
77✔
534
                        uniqueName = ri.cv.Spec.ComponentRef.Name
6✔
535
                }
6✔
536

537
                namedGroups[uniqueName] = append(namedGroups[uniqueName], ri)
71✔
538
        }
539

540
        var accepted []releaseInfo
47✔
541

47✔
542
        var skipped []string
47✔
543

47✔
544
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
47✔
545
        byPriority := func(a, b releaseInfo) bool {
71✔
546
                if a.release.Spec.Priority != b.release.Spec.Priority {
34✔
547
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
548
                }
10✔
549

550
                return a.bindingKey < b.bindingKey
14✔
551
        }
552

553
        uniqueNames := make([]string, 0, len(namedGroups))
47✔
554
        for k := range namedGroups {
109✔
555
                uniqueNames = append(uniqueNames, k)
62✔
556
        }
62✔
557

558
        sort.Strings(uniqueNames)
47✔
559

47✔
560
        for _, uniqueName := range uniqueNames {
109✔
561
                group := namedGroups[uniqueName]
62✔
562
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
71✔
563

564
                accepted = append(accepted, group[0])
62✔
565

62✔
566
                for _, loser := range group[1:] {
71✔
567
                        skipped = append(skipped, fmt.Sprintf(
9✔
568
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
569
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
570
                        ))
9✔
571
                }
9✔
572
        }
573

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

579
        resolved := make([]releaseInfo, 0, len(accepted))
47✔
580

47✔
581
        for _, ri := range accepted {
109✔
582
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
62✔
583
                var riSelector labels.Selector
62✔
584
                if ri.release.Spec.AntiAffinity != nil {
69✔
585
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
586
                        if err != nil {
8✔
587
                                skipped = append(skipped, fmt.Sprintf(
1✔
588
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
589
                                        ri.bindingKey, err,
1✔
590
                                ))
1✔
591

1✔
592
                                continue
1✔
593
                        }
594

595
                        riSelector = sel
6✔
596
                }
597

598
                // Check both directions: ri's anti-affinity against already-resolved labels,
599
                // and already-resolved anti-affinities against ri's labels.
600
                conflict := ""
61✔
601
                for _, other := range resolved {
76✔
602
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
19✔
603
                                conflict = other.bindingKey
4✔
604
                                break
4✔
605
                        }
606

607
                        if other.release.Spec.AntiAffinity != nil {
12✔
608
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
609
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
610
                                        conflict = other.bindingKey
1✔
611
                                        break
1✔
612
                                }
613
                        }
614
                }
615

616
                if conflict != "" {
66✔
617
                        skipped = append(skipped, fmt.Sprintf(
5✔
618
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
619
                                ri.bindingKey, conflict,
5✔
620
                        ))
5✔
621
                } else {
61✔
622
                        resolved = append(resolved, ri)
56✔
623
                }
56✔
624
        }
625

626
        return resolved, skipped
47✔
627
}
628

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

5✔
635
        rtList := &solarv1alpha1.RenderTaskList{}
5✔
636
        if err := r.List(ctx, rtList,
5✔
637
                client.InNamespace(target.Namespace),
5✔
638
                client.MatchingFields{indexOwnerKind: "Target"},
5✔
639
        ); err != nil {
5✔
640
                return err
×
641
        }
×
642

643
        for i := range rtList.Items {
19✔
644
                rt := &rtList.Items[i]
14✔
645
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
14✔
646
                        continue
×
647
                }
648

649
                if _, current := currentRTNames[rt.Name]; current {
27✔
650
                        continue
13✔
651
                }
652

653
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
654
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
655
                        return err
×
656
                }
×
657

658
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
659
                        "Deleted stale RenderTask %s", rt.Name)
1✔
660
        }
661

662
        return nil
5✔
663
}
664

665
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
666
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
667
        if err := r.List(ctx, rtList,
1✔
668
                client.InNamespace(target.Namespace),
1✔
669
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
670
        ); err != nil {
1✔
671
                return err
×
672
        }
×
673

674
        for i := range rtList.Items {
1✔
675
                rt := &rtList.Items[i]
×
676
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
677
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
678
                                return err
×
679
                        }
×
680
                }
681
        }
682

683
        return nil
1✔
684
}
685

686
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec {
10✔
687
        chartName := fmt.Sprintf("release-%s", rel.Name)
10✔
688
        repo := fmt.Sprintf("%s/%s/%s", target.Namespace, rel.Namespace, chartName)
10✔
689
        tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration())
10✔
690

10✔
691
        var targetNamespace string
10✔
692
        if rel.Spec.TargetNamespace != nil {
20✔
693
                targetNamespace = *rel.Spec.TargetNamespace
10✔
694
        }
10✔
695

696
        return solarv1alpha1.RenderTaskSpec{
10✔
697
                RendererConfig: solarv1alpha1.RendererConfig{
10✔
698
                        Type: solarv1alpha1.RendererConfigTypeRelease,
10✔
699
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
10✔
700
                                Chart: solarv1alpha1.ChartConfig{
10✔
701
                                        Name:        chartName,
10✔
702
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
10✔
703
                                        Version:     tag,
10✔
704
                                        AppVersion:  tag,
10✔
705
                                },
10✔
706
                                Input: solarv1alpha1.ReleaseInput{
10✔
707
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
10✔
708
                                        Resources:  cv.Spec.Resources,
10✔
709
                                        Entrypoint: cv.Spec.Entrypoint,
10✔
710
                                },
10✔
711
                                Values:          rel.Spec.Values,
10✔
712
                                TargetNamespace: targetNamespace,
10✔
713
                        },
10✔
714
                },
10✔
715
                Repository:     repo,
10✔
716
                Tag:            tag,
10✔
717
                BaseURL:        registry.Spec.Hostname,
10✔
718
                PushSecretRef:  registry.Spec.SolarSecretRef,
10✔
719
                FailedJobTTL:   rel.Spec.FailedJobTTL,
10✔
720
                OwnerName:      target.Name,
10✔
721
                OwnerNamespace: target.Namespace,
10✔
722
                OwnerKind:      "Target",
10✔
723
        }
10✔
724
}
725

726
// buildBootstrapInput constructs the desired BootstrapInput from the current
727
// target and resolved releases. Used for both comparison and spec construction.
728
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
13✔
729
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
13✔
730

13✔
731
        for _, ri := range releases {
34✔
732
                ref, err := ociname.ParseReference(ri.chartURL)
21✔
733
                if err != nil {
21✔
734
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
735
                }
×
736

737
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
21✔
738
                if err != nil {
21✔
739
                        return solarv1alpha1.BootstrapInput{}, err
×
740
                }
×
741

742
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
21✔
743
                        Repository: strings.TrimPrefix(repo, "oci://"),
21✔
744
                        Tag:        ref.Identifier(),
21✔
745
                }
21✔
746
        }
747

748
        return solarv1alpha1.BootstrapInput{
13✔
749
                Releases: resolvedReleases,
13✔
750
                Userdata: target.Spec.Userdata,
13✔
751
        }, nil
13✔
752
}
753

754
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
2✔
755
        input, err := buildBootstrapInput(target, releases)
2✔
756
        if err != nil {
2✔
757
                return solarv1alpha1.RenderTaskSpec{}, err
×
758
        }
×
759

760
        releaseNames := make([]string, 0, len(releases))
2✔
761
        for _, ri := range releases {
5✔
762
                releaseNames = append(releaseNames, ri.name)
3✔
763
        }
3✔
764

765
        sort.Strings(releaseNames)
2✔
766

2✔
767
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
2✔
768
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
2✔
769
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
2✔
770

2✔
771
        return solarv1alpha1.RenderTaskSpec{
2✔
772
                RendererConfig: solarv1alpha1.RendererConfig{
2✔
773
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
2✔
774
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
2✔
775
                                Chart: solarv1alpha1.ChartConfig{
2✔
776
                                        Name:        chartName,
2✔
777
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
2✔
778
                                        Version:     tag,
2✔
779
                                        AppVersion:  tag,
2✔
780
                                },
2✔
781
                                Input: input,
2✔
782
                        },
2✔
783
                },
2✔
784
                Repository:     repo,
2✔
785
                Tag:            tag,
2✔
786
                BaseURL:        registry.Spec.Hostname,
2✔
787
                PushSecretRef:  registry.Spec.SolarSecretRef,
2✔
788
                OwnerName:      target.Name,
2✔
789
                OwnerNamespace: target.Namespace,
2✔
790
                OwnerKind:      "Target",
2✔
791
        }, nil
2✔
792
}
793

794
// SetupWithManager sets up the controller with the Manager.
795
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
796
        return ctrl.NewControllerManagedBy(mgr).
1✔
797
                For(&solarv1alpha1.Target{}).
1✔
798
                Watches(
1✔
799
                        &solarv1alpha1.ReleaseBinding{},
1✔
800
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
801
                ).
1✔
802
                Watches(
1✔
803
                        &solarv1alpha1.RenderTask{},
1✔
804
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
805
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
806
                ).
1✔
807
                Watches(
1✔
808
                        &solarv1alpha1.Registry{},
1✔
809
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
810
                ).
1✔
811
                Watches(
1✔
812
                        &solarv1alpha1.ReferenceGrant{},
1✔
813
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
814
                ).
1✔
815
                Watches(
1✔
816
                        &solarv1alpha1.Release{},
1✔
817
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
818
                ).
1✔
819
                Complete(r)
1✔
820
}
1✔
821

822
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
823
// fromNamespace to reference the named registry.
824
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
825
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
826
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
827
                return false, err
×
828
        }
×
829
        for i := range grantList.Items {
×
830
                grant := &grantList.Items[i]
×
831
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
832
                        return true, nil
×
833
                }
×
834
        }
835

836
        return false, nil
×
837
}
838

839
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
840
// fromNamespace to reference Registry resources in the grant's namespace.
841
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
842
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
843
}
×
844

845
// mapRegistryToTargets maps a Registry event to reconcile requests for all
846
// Targets that reference it — either in the same namespace or cross-namespace.
847
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
15✔
848
        reg, ok := obj.(*solarv1alpha1.Registry)
15✔
849
        if !ok {
15✔
850
                return nil
×
851
        }
×
852

853
        // Same-namespace targets
854
        targetList := &solarv1alpha1.TargetList{}
15✔
855
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
15✔
856
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
857

×
858
                return nil
×
859
        }
×
860

861
        var requests []reconcile.Request
15✔
862
        for _, t := range targetList.Items {
15✔
863
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
×
864
                        (t.Spec.RenderRegistryNamespace == "" || 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
        // Cross-namespace targets: find namespaces that have been granted access to
875
        // registries in reg.Namespace, then check their targets.
876
        grantList := &solarv1alpha1.ReferenceGrantList{}
15✔
877
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
15✔
878
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
879
                return requests
×
880
        }
×
881

882
        for i := range grantList.Items {
15✔
883
                grant := &grantList.Items[i]
×
884
                if !grantsRegistryResource(grant) {
×
885
                        continue
×
886
                }
887
                for _, from := range grant.Spec.From {
×
888
                        if from.Kind != "Target" || from.Group != solarGroup {
×
889
                                continue
×
890
                        }
891
                        crossTargets := &solarv1alpha1.TargetList{}
×
892
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
893
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
894
                                continue
×
895
                        }
896
                        for _, t := range crossTargets.Items {
×
897
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
898
                                        requests = append(requests, reconcile.Request{
×
899
                                                NamespacedName: types.NamespacedName{
×
900
                                                        Name:      t.Name,
×
901
                                                        Namespace: t.Namespace,
×
902
                                                },
×
903
                                        })
×
904
                                }
×
905
                        }
906
                }
907
        }
908

909
        return requests
15✔
910
}
911

912
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
913
// either because the grant controls Registry access (Target → Registry) or because
914
// it controls ComponentVersion access (Release → ComponentVersion).
915
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
13✔
916
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
13✔
917
        if !ok {
13✔
918
                return nil
×
919
        }
×
920

921
        var requests []reconcile.Request
13✔
922

13✔
923
        if grantsRegistryResource(grant) {
13✔
924
                for _, from := range grant.Spec.From {
×
925
                        if from.Kind != "Target" || from.Group != solarGroup {
×
926
                                continue
×
927
                        }
928
                        targets := &solarv1alpha1.TargetList{}
×
929
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
930
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
931
                                continue
×
932
                        }
933
                        for _, t := range targets.Items {
×
934
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
935
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
936
                                        requests = append(requests, reconcile.Request{
×
937
                                                NamespacedName: types.NamespacedName{
×
938
                                                        Name:      t.Name,
×
939
                                                        Namespace: t.Namespace,
×
940
                                                },
×
941
                                        })
×
942
                                }
×
943
                        }
944
                }
945
        }
946

947
        if grantsComponentVersionResource(grant) {
18✔
948
                seen := map[string]struct{}{}
5✔
949
                for _, from := range grant.Spec.From {
10✔
950
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
951
                                continue
×
952
                        }
953
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
954
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
NEW
955
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
956
                                continue
×
957
                        }
958
                        for _, rb := range bindings.Items {
6✔
959
                                if rb.Spec.TargetRef.Name == "" {
1✔
NEW
960
                                        continue
×
961
                                }
962
                                targetNs := rb.Namespace
1✔
963
                                if rb.Spec.TargetNamespace != "" {
2✔
964
                                        targetNs = rb.Spec.TargetNamespace
1✔
965
                                }
1✔
966
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
967
                                if _, ok := seen[key]; ok {
1✔
NEW
968
                                        continue
×
969
                                }
970
                                seen[key] = struct{}{}
1✔
971
                                requests = append(requests, reconcile.Request{
1✔
972
                                        NamespacedName: types.NamespacedName{
1✔
973
                                                Name:      rb.Spec.TargetRef.Name,
1✔
974
                                                Namespace: targetNs,
1✔
975
                                        },
1✔
976
                                })
1✔
977
                        }
978
                }
979
        }
980

981
        if grantsReleaseBindingToTargetResource(grant) {
17✔
982
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
983
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
984
                // the new or removed cross-namespace ReleaseBindings.
4✔
985
                targets := &solarv1alpha1.TargetList{}
4✔
986
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
NEW
987
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
988
                } else {
4✔
989
                        for _, t := range targets.Items {
8✔
990
                                requests = append(requests, reconcile.Request{
4✔
991
                                        NamespacedName: types.NamespacedName{
4✔
992
                                                Name:      t.Name,
4✔
993
                                                Namespace: t.Namespace,
4✔
994
                                        },
4✔
995
                                })
4✔
996
                        }
4✔
997
                }
998
        }
999

1000
        return requests
13✔
1001
}
1002

1003
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1004
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
13✔
1005
        for _, t := range grant.Spec.To {
26✔
1006
                if t.Kind == "Registry" && t.Group == solarGroup {
13✔
1007
                        return true
×
1008
                }
×
1009
        }
1010

1011
        return false
13✔
1012
}
1013

1014
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1015
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1016
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
24✔
1017
        hasReleaseBindingFrom := false
24✔
1018
        for _, f := range grant.Spec.From {
48✔
1019
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
39✔
1020
                        hasReleaseBindingFrom = true
15✔
1021
                        break
15✔
1022
                }
1023
        }
1024
        if !hasReleaseBindingFrom {
33✔
1025
                return false
9✔
1026
        }
9✔
1027
        for _, t := range grant.Spec.To {
30✔
1028
                if t.Kind == "Target" && t.Group == solarGroup {
30✔
1029
                        return true
15✔
1030
                }
15✔
1031
        }
1032

NEW
1033
        return false
×
1034
}
1035

1036
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1037
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1038
// a ReferenceGrant in target's namespace.
1039
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
50✔
1040
        grantList := &solarv1alpha1.ReferenceGrantList{}
50✔
1041
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
50✔
NEW
1042
                return nil, err
×
NEW
1043
        }
×
1044

1045
        seen := make(map[string]struct{})
50✔
1046
        var result []solarv1alpha1.ReleaseBinding
50✔
1047
        for i := range grantList.Items {
61✔
1048
                grant := &grantList.Items[i]
11✔
1049
                if !grantsReleaseBindingToTargetResource(grant) {
11✔
NEW
1050
                        continue
×
1051
                }
1052
                for _, from := range grant.Spec.From {
22✔
1053
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
11✔
NEW
1054
                                continue
×
1055
                        }
1056
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
11✔
1057
                        if err := r.List(ctx, crossBindings,
11✔
1058
                                client.InNamespace(from.Namespace),
11✔
1059
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
11✔
1060
                        ); err != nil {
11✔
NEW
1061
                                return nil, err
×
NEW
1062
                        }
×
1063
                        for _, rb := range crossBindings.Items {
19✔
1064
                                if rb.Spec.TargetNamespace != target.Namespace {
8✔
NEW
1065
                                        continue
×
1066
                                }
1067
                                key := rb.Namespace + "/" + rb.Name
8✔
1068
                                if _, exists := seen[key]; exists {
9✔
1069
                                        continue
1✔
1070
                                }
1071
                                seen[key] = struct{}{}
7✔
1072
                                result = append(result, rb)
7✔
1073
                        }
1074
                }
1075
        }
1076

1077
        return result, nil
50✔
1078
}
1079

1080
// mapReleaseToTargets maps a Release event to reconcile requests for all
1081
// Targets that are bound to the release via ReleaseBindings.
1082
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
65✔
1083
        rel, ok := obj.(*solarv1alpha1.Release)
65✔
1084
        if !ok {
65✔
1085
                return nil
×
1086
        }
×
1087

1088
        bindingList := &solarv1alpha1.ReleaseBindingList{}
65✔
1089
        if err := r.List(ctx, bindingList,
65✔
1090
                client.InNamespace(rel.Namespace),
65✔
1091
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
65✔
1092
        ); err != nil {
65✔
1093
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1094

×
1095
                return nil
×
1096
        }
×
1097

1098
        seen := map[string]struct{}{}
65✔
1099
        var requests []reconcile.Request
65✔
1100

65✔
1101
        for _, rb := range bindingList.Items {
65✔
NEW
1102
                targetNs := rb.Namespace
×
NEW
1103
                if rb.Spec.TargetNamespace != "" {
×
NEW
1104
                        targetNs = rb.Spec.TargetNamespace
×
NEW
1105
                }
×
1106

NEW
1107
                key := targetNs + "/" + rb.Spec.TargetRef.Name
×
NEW
1108
                if _, ok := seen[key]; ok {
×
1109
                        continue
×
1110
                }
1111

NEW
1112
                seen[key] = struct{}{}
×
1113
                requests = append(requests, reconcile.Request{
×
1114
                        NamespacedName: types.NamespacedName{
×
NEW
1115
                                Name:      rb.Spec.TargetRef.Name,
×
NEW
1116
                                Namespace: targetNs,
×
1117
                        },
×
1118
                })
×
1119
        }
1120

1121
        return requests
65✔
1122
}
1123

1124
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
26✔
1125
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
26✔
1126
        if !ok || rb.Spec.TargetRef.Name == "" {
26✔
1127
                return nil
×
1128
        }
×
1129

1130
        targetNs := rb.Namespace
26✔
1131
        if rb.Spec.TargetNamespace != "" {
35✔
1132
                targetNs = rb.Spec.TargetNamespace
9✔
1133
        }
9✔
1134

1135
        return []reconcile.Request{
26✔
1136
                {
26✔
1137
                        NamespacedName: types.NamespacedName{
26✔
1138
                                Name:      rb.Spec.TargetRef.Name,
26✔
1139
                                Namespace: targetNs,
26✔
1140
                        },
26✔
1141
                },
26✔
1142
        }
26✔
1143
}
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