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

opendefensecloud / solution-arsenal / 25494381042

07 May 2026 11:58AM UTC coverage: 71.355% (+0.6%) from 70.732%
25494381042

push

github

web-flow
feat(controller): implement release resolver in Target controller (#246) (#494)

## What

Adds a deterministic release resolver to the Target controller that runs
after binding collection and before RenderTask creation. Implements
uniqueName-based deduplication (priority wins; bindingKey tiebreaker)
and bidirectional anti-affinity enforcement.

Also makes `spec.uniqueName` optional on Release: when not set, the
effective unique name falls back to the parent Component name from the
referenced ComponentVersion, written to `status.effectiveUniqueName` for
operator visibility.

## Why
Without conflict resolution, two Profiles can bind the same Target to
two versions of the same component (e.g., kyverno v3.2 and v3.3),
resulting in duplicate RenderTasks and unpredictable deployments. The
resolver enforces that only one Release per logical component reaches
the render stage, with the selection governed by explicit priority and
anti-affinity rules.

`UniqueName` being optional reduces friction for simple single-profile
setups — the system derives a sensible default automatically and exposes
it in `status.effectiveUniqueName` so operators can verify without
reading docs.

## Testing
- **Unit tests** (`resolveReleaseConflicts`): priority selection,
bindingKey tiebreaker, bidirectional anti-affinity, invalid selector,
component-name fallback for empty UniqueName.
- **Integration tests** (envtest): higher-priority wins on conflict,
alphabetical tiebreaker, anti-affinity blocking, NoConflicts path,
optional UniqueName + status.effectiveUniqueName fallback.

Adding e2e scenarios for the resolver was considered but skipped: the
resolver is a pure in-process filter with no external I/O. Setting up
multiple OCI-registered components, Profiles, and Releases in a live
Kind cluster just to test a sort/filter function would add significant
fixture overhead for no additional coverage beyond what the envtest
integration tests already provide.

## Notes for reviewers
**New ... (continued)

99 of 108 new or added lines in 2 files covered. (91.67%)

2 existing lines in 1 file now uncovered.

2354 of 3299 relevant lines covered (71.35%)

18.12 hits per line

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

67.77
/pkg/controller/target_controller.go
1
// Copyright 2026 BWI GmbH and Solution Arsenal contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "errors"
9
        "fmt"
10
        "net/url"
11
        "slices"
12
        "sort"
13
        "strings"
14
        "time"
15

16
        ociname "github.com/google/go-containerregistry/pkg/name"
17
        corev1 "k8s.io/api/core/v1"
18
        apiequality "k8s.io/apimachinery/pkg/api/equality"
19
        apierrors "k8s.io/apimachinery/pkg/api/errors"
20
        apimeta "k8s.io/apimachinery/pkg/api/meta"
21
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22
        "k8s.io/apimachinery/pkg/labels"
23
        "k8s.io/apimachinery/pkg/runtime"
24
        "k8s.io/apimachinery/pkg/types"
25
        "k8s.io/client-go/tools/events"
26
        ctrl "sigs.k8s.io/controller-runtime"
27
        "sigs.k8s.io/controller-runtime/pkg/builder"
28
        "sigs.k8s.io/controller-runtime/pkg/client"
29
        "sigs.k8s.io/controller-runtime/pkg/handler"
30
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
31

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

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

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

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

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

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

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

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

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

130✔
86
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
175✔
87
                return ctrl.Result{}, nil
45✔
88
        }
45✔
89

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

215
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
8✔
216
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
217
                        return ctrl.Result{}, condErr
×
218
                }
×
219

220
                return ctrl.Result{}, nil
8✔
221
        }
222

223
        // For each bound release, ensure a per-release RenderTask exists
224
        var releases []releaseInfo
28✔
225

28✔
226
        pendingDeps := false
28✔
227

28✔
228
        for _, binding := range bindingList.Items {
69✔
229
                rel := &solarv1alpha1.Release{}
41✔
230
                if err := r.Get(ctx, client.ObjectKey{
41✔
231
                        Name:      binding.Spec.ReleaseRef.Name,
41✔
232
                        Namespace: target.Namespace,
41✔
233
                }, rel); err != nil {
41✔
234
                        if apierrors.IsNotFound(err) {
×
235
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
236
                                pendingDeps = true
×
237

×
238
                                continue
×
239
                        }
240

241
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
242
                }
243

244
                cv := &solarv1alpha1.ComponentVersion{}
41✔
245
                cvNamespace := target.Namespace
41✔
246
                if rel.Spec.ComponentVersionNamespace != "" {
41✔
247
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
248
                }
×
249

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

×
265
                                continue
×
266
                        }
267
                }
268

269
                if err := r.Get(ctx, client.ObjectKey{
41✔
270
                        Name:      rel.Spec.ComponentVersionRef.Name,
41✔
271
                        Namespace: cvNamespace,
41✔
272
                }, cv); err != nil {
41✔
273
                        if apierrors.IsNotFound(err) {
×
274
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
275
                                pendingDeps = true
×
276

×
277
                                continue
×
278
                        }
279

280
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
281
                }
282

283
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
41✔
284
                releases = append(releases, releaseInfo{
41✔
285
                        bindingKey: binding.Namespace + "/" + binding.Name,
41✔
286
                        name:       rel.Name,
41✔
287
                        release:    rel,
41✔
288
                        cv:         cv,
41✔
289
                        rtName:     rtName,
41✔
290
                })
41✔
291
        }
292

293
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
294
        var skipped []string
28✔
295
        releases, skipped = resolveReleaseConflicts(releases)
28✔
296
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
28✔
NEW
297
                return ctrl.Result{}, condErr
×
NEW
298
        }
×
299

300
        if len(releases) == 0 && !pendingDeps {
28✔
NEW
301
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
NEW
302
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
NEW
303
                        return ctrl.Result{}, condErr
×
NEW
304
                }
×
305

NEW
306
                return ctrl.Result{}, nil
×
307
        }
308

309
        // Create per-release RenderTasks (one per target+release pair).
310
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
311
        allRendered := true
28✔
312

28✔
313
        for i, ri := range releases {
60✔
314
                rt := &solarv1alpha1.RenderTask{}
32✔
315
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
32✔
316

32✔
317
                if apierrors.IsNotFound(err) {
39✔
318
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
7✔
319
                        rt = &solarv1alpha1.RenderTask{
7✔
320
                                ObjectMeta: metav1.ObjectMeta{
7✔
321
                                        Name:      ri.rtName,
7✔
322
                                        Namespace: target.Namespace,
7✔
323
                                },
7✔
324
                                Spec: spec,
7✔
325
                        }
7✔
326

7✔
327
                        if err := r.Create(ctx, rt); err != nil {
7✔
328
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
329
                        }
×
330

331
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
7✔
332
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
333
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
7✔
334
                } else if err != nil {
25✔
335
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
336
                }
×
337

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

345
                        return ctrl.Result{}, nil
×
346
                }
347

348
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
45✔
349
                        releases[i].chartURL = rt.Status.ChartURL
13✔
350
                } else {
32✔
351
                        allRendered = false
19✔
352
                }
19✔
353
        }
354

355
        if pendingDeps {
28✔
356
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
357
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
358
                        return ctrl.Result{}, condErr
×
359
                }
×
360

361
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
362
        }
363

364
        if !allRendered {
47✔
365
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
19✔
366
                        "Waiting for release RenderTasks to complete"); condErr != nil {
21✔
367
                        return ctrl.Result{}, condErr
2✔
368
                }
2✔
369

370
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
17✔
371
        }
372

373
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
9✔
374
                "All releases rendered successfully"); condErr != nil {
9✔
375
                return ctrl.Result{}, condErr
×
376
        }
×
377

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

9✔
385
        needsNewBootstrap := false
9✔
386

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

401
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
8✔
402
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
10✔
403
                        bootstrapVersion++
2✔
404
                        needsNewBootstrap = true
2✔
405
                }
2✔
406
        }
407

408
        if needsNewBootstrap {
12✔
409
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
3✔
410
                if specErr != nil {
3✔
411
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
412
                }
×
413

414
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
3✔
415
                bootstrapRT = &solarv1alpha1.RenderTask{
3✔
416
                        ObjectMeta: metav1.ObjectMeta{
3✔
417
                                Name:      bootstrapRTName,
3✔
418
                                Namespace: target.Namespace,
3✔
419
                        },
3✔
420
                        Spec: spec,
3✔
421
                }
3✔
422

3✔
423
                if err := r.Create(ctx, bootstrapRT); err != nil {
4✔
424
                        if !apierrors.IsAlreadyExists(err) {
1✔
425
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
426
                        }
×
427

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

437
                // Persist the new bootstrapVersion in status
438
                if bootstrapVersion != target.Status.BootstrapVersion {
5✔
439
                        target.Status.BootstrapVersion = bootstrapVersion
2✔
440
                        if err := r.Status().Update(ctx, target); err != nil {
3✔
441
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
1✔
442
                        }
1✔
443
                }
444
        }
445

446
        // Update target status from bootstrap RenderTask
447
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
8✔
448
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
449
                        "Bootstrap rendering failed"); condErr != nil {
×
450
                        return ctrl.Result{}, condErr
×
451
                }
×
452

453
                return ctrl.Result{}, nil
×
454
        }
455

456
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
12✔
457
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
4✔
458
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
4✔
459
                        return ctrl.Result{}, condErr
×
460
                }
×
461

462
                // Clean up stale RenderTasks owned by this target (old versions)
463
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
4✔
464
                for _, ri := range releases {
10✔
465
                        currentRTNames[ri.rtName] = struct{}{}
6✔
466
                }
6✔
467
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
4✔
468
                        log.Error(err, "failed to clean up stale RenderTasks")
×
469
                }
×
470

471
                return ctrl.Result{}, nil
4✔
472
        }
473

474
        // Still running
475
        return ctrl.Result{}, nil
4✔
476
}
477

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

492
        return nil
131✔
493
}
494

495
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
28✔
496
        if len(skipped) == 0 {
47✔
497
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
19✔
498
        }
19✔
499

500
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
501
}
502

503
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
504
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
505
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
506
// It returns the accepted releases and a slice of human-readable filter messages.
507
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
36✔
508
        if len(releases) == 0 {
37✔
509
                return releases, nil
1✔
510
        }
1✔
511

512
        // Step A: uniqueName deduplication.
513
        // When UniqueName is empty, fall back to the parent Component name from the CV.
514
        namedGroups := map[string][]releaseInfo{}
35✔
515

35✔
516
        for _, ri := range releases {
88✔
517
                uniqueName := ri.release.Spec.UniqueName
53✔
518
                if uniqueName == "" {
59✔
519
                        uniqueName = ri.cv.Spec.ComponentRef.Name
6✔
520
                }
6✔
521

522
                namedGroups[uniqueName] = append(namedGroups[uniqueName], ri)
53✔
523
        }
524

525
        var accepted []releaseInfo
35✔
526

35✔
527
        var skipped []string
35✔
528

35✔
529
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
35✔
530
        byPriority := func(a, b releaseInfo) bool {
53✔
531
                if a.release.Spec.Priority != b.release.Spec.Priority {
28✔
532
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
533
                }
10✔
534

535
                return a.bindingKey < b.bindingKey
8✔
536
        }
537

538
        uniqueNames := make([]string, 0, len(namedGroups))
35✔
539
        for k := range namedGroups {
79✔
540
                uniqueNames = append(uniqueNames, k)
44✔
541
        }
44✔
542

543
        sort.Strings(uniqueNames)
35✔
544

35✔
545
        for _, uniqueName := range uniqueNames {
79✔
546
                group := namedGroups[uniqueName]
44✔
547
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
53✔
548

549
                accepted = append(accepted, group[0])
44✔
550

44✔
551
                for _, loser := range group[1:] {
53✔
552
                        skipped = append(skipped, fmt.Sprintf(
9✔
553
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
554
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
555
                        ))
9✔
556
                }
9✔
557
        }
558

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

564
        resolved := make([]releaseInfo, 0, len(accepted))
35✔
565

35✔
566
        for _, ri := range accepted {
79✔
567
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
44✔
568
                var riSelector labels.Selector
44✔
569
                if ri.release.Spec.AntiAffinity != nil {
51✔
570
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
571
                        if err != nil {
8✔
572
                                skipped = append(skipped, fmt.Sprintf(
1✔
573
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
574
                                        ri.bindingKey, err,
1✔
575
                                ))
1✔
576

1✔
577
                                continue
1✔
578
                        }
579

580
                        riSelector = sel
6✔
581
                }
582

583
                // Check both directions: ri's anti-affinity against already-resolved labels,
584
                // and already-resolved anti-affinities against ri's labels.
585
                conflict := ""
43✔
586
                for _, other := range resolved {
52✔
587
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
13✔
588
                                conflict = other.bindingKey
4✔
589
                                break
4✔
590
                        }
591

592
                        if other.release.Spec.AntiAffinity != nil {
6✔
593
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
594
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
595
                                        conflict = other.bindingKey
1✔
596
                                        break
1✔
597
                                }
598
                        }
599
                }
600

601
                if conflict != "" {
48✔
602
                        skipped = append(skipped, fmt.Sprintf(
5✔
603
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
604
                                ri.bindingKey, conflict,
5✔
605
                        ))
5✔
606
                } else {
43✔
607
                        resolved = append(resolved, ri)
38✔
608
                }
38✔
609
        }
610

611
        return resolved, skipped
35✔
612
}
613

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

4✔
620
        rtList := &solarv1alpha1.RenderTaskList{}
4✔
621
        if err := r.List(ctx, rtList,
4✔
622
                client.InNamespace(target.Namespace),
4✔
623
                client.MatchingFields{indexOwnerKind: "Target"},
4✔
624
        ); err != nil {
4✔
625
                return err
×
626
        }
×
627

628
        for i := range rtList.Items {
15✔
629
                rt := &rtList.Items[i]
11✔
630
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
11✔
631
                        continue
×
632
                }
633

634
                if _, current := currentRTNames[rt.Name]; current {
21✔
635
                        continue
10✔
636
                }
637

638
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
639
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
640
                        return err
×
641
                }
×
642

643
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
644
                        "Deleted stale RenderTask %s", rt.Name)
1✔
645
        }
646

647
        return nil
4✔
648
}
649

650
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
651
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
652
        if err := r.List(ctx, rtList,
1✔
653
                client.InNamespace(target.Namespace),
1✔
654
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
655
        ); err != nil {
1✔
656
                return err
×
657
        }
×
658

659
        for i := range rtList.Items {
1✔
660
                rt := &rtList.Items[i]
×
661
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
662
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
663
                                return err
×
664
                        }
×
665
                }
666
        }
667

668
        return nil
1✔
669
}
670

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

7✔
676
        var targetNamespace string
7✔
677
        if rel.Spec.TargetNamespace != nil {
14✔
678
                targetNamespace = *rel.Spec.TargetNamespace
7✔
679
        }
7✔
680

681
        return solarv1alpha1.RenderTaskSpec{
7✔
682
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
683
                        Type: solarv1alpha1.RendererConfigTypeRelease,
7✔
684
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
7✔
685
                                Chart: solarv1alpha1.ChartConfig{
7✔
686
                                        Name:        chartName,
7✔
687
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
7✔
688
                                        Version:     tag,
7✔
689
                                        AppVersion:  tag,
7✔
690
                                },
7✔
691
                                Input: solarv1alpha1.ReleaseInput{
7✔
692
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
7✔
693
                                        Resources:  cv.Spec.Resources,
7✔
694
                                        Entrypoint: cv.Spec.Entrypoint,
7✔
695
                                },
7✔
696
                                Values:          rel.Spec.Values,
7✔
697
                                TargetNamespace: targetNamespace,
7✔
698
                        },
7✔
699
                },
7✔
700
                Repository:     repo,
7✔
701
                Tag:            tag,
7✔
702
                BaseURL:        registry.Spec.Hostname,
7✔
703
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
704
                FailedJobTTL:   rel.Spec.FailedJobTTL,
7✔
705
                OwnerName:      target.Name,
7✔
706
                OwnerNamespace: target.Namespace,
7✔
707
                OwnerKind:      "Target",
7✔
708
        }
7✔
709
}
710

711
// buildBootstrapInput constructs the desired BootstrapInput from the current
712
// target and resolved releases. Used for both comparison and spec construction.
713
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
11✔
714
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
11✔
715

11✔
716
        for _, ri := range releases {
27✔
717
                ref, err := ociname.ParseReference(ri.chartURL)
16✔
718
                if err != nil {
16✔
719
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
720
                }
×
721

722
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
16✔
723
                if err != nil {
16✔
724
                        return solarv1alpha1.BootstrapInput{}, err
×
725
                }
×
726

727
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
16✔
728
                        Repository: strings.TrimPrefix(repo, "oci://"),
16✔
729
                        Tag:        ref.Identifier(),
16✔
730
                }
16✔
731
        }
732

733
        return solarv1alpha1.BootstrapInput{
11✔
734
                Releases: resolvedReleases,
11✔
735
                Userdata: target.Spec.Userdata,
11✔
736
        }, nil
11✔
737
}
738

739
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
3✔
740
        input, err := buildBootstrapInput(target, releases)
3✔
741
        if err != nil {
3✔
742
                return solarv1alpha1.RenderTaskSpec{}, err
×
743
        }
×
744

745
        releaseNames := make([]string, 0, len(releases))
3✔
746
        for _, ri := range releases {
8✔
747
                releaseNames = append(releaseNames, ri.name)
5✔
748
        }
5✔
749

750
        sort.Strings(releaseNames)
3✔
751

3✔
752
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
3✔
753
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
3✔
754
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
3✔
755

3✔
756
        return solarv1alpha1.RenderTaskSpec{
3✔
757
                RendererConfig: solarv1alpha1.RendererConfig{
3✔
758
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
3✔
759
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
3✔
760
                                Chart: solarv1alpha1.ChartConfig{
3✔
761
                                        Name:        chartName,
3✔
762
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
3✔
763
                                        Version:     tag,
3✔
764
                                        AppVersion:  tag,
3✔
765
                                },
3✔
766
                                Input: input,
3✔
767
                        },
3✔
768
                },
3✔
769
                Repository:     repo,
3✔
770
                Tag:            tag,
3✔
771
                BaseURL:        registry.Spec.Hostname,
3✔
772
                PushSecretRef:  registry.Spec.SolarSecretRef,
3✔
773
                OwnerName:      target.Name,
3✔
774
                OwnerNamespace: target.Namespace,
3✔
775
                OwnerKind:      "Target",
3✔
776
        }, nil
3✔
777
}
778

779
// SetupWithManager sets up the controller with the Manager.
780
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
781
        return ctrl.NewControllerManagedBy(mgr).
1✔
782
                For(&solarv1alpha1.Target{}).
1✔
783
                Watches(
1✔
784
                        &solarv1alpha1.ReleaseBinding{},
1✔
785
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
786
                ).
1✔
787
                Watches(
1✔
788
                        &solarv1alpha1.RenderTask{},
1✔
789
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
790
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
791
                ).
1✔
792
                Watches(
1✔
793
                        &solarv1alpha1.Registry{},
1✔
794
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
795
                ).
1✔
796
                Watches(
1✔
797
                        &solarv1alpha1.ReferenceGrant{},
1✔
798
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
799
                ).
1✔
800
                Watches(
1✔
801
                        &solarv1alpha1.Release{},
1✔
802
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
803
                ).
1✔
804
                Complete(r)
1✔
805
}
1✔
806

807
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
808
// fromNamespace to reference the named registry.
809
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
810
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
811
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
812
                return false, err
×
813
        }
×
814
        for i := range grantList.Items {
×
815
                grant := &grantList.Items[i]
×
816
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
817
                        return true, nil
×
818
                }
×
819
        }
820

821
        return false, nil
×
822
}
823

824
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
825
// fromNamespace to reference Registry resources in the grant's namespace.
826
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
827
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
828
}
×
829

830
// mapRegistryToTargets maps a Registry event to reconcile requests for all
831
// Targets that reference it — either in the same namespace or cross-namespace.
832
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
10✔
833
        reg, ok := obj.(*solarv1alpha1.Registry)
10✔
834
        if !ok {
10✔
835
                return nil
×
836
        }
×
837

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

×
843
                return nil
×
844
        }
×
845

846
        var requests []reconcile.Request
10✔
847
        for _, t := range targetList.Items {
10✔
848
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
×
849
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
×
850
                        requests = append(requests, reconcile.Request{
×
851
                                NamespacedName: types.NamespacedName{
×
852
                                        Name:      t.Name,
×
853
                                        Namespace: t.Namespace,
×
854
                                },
×
855
                        })
×
856
                }
×
857
        }
858

859
        // Cross-namespace targets: find namespaces that have been granted access to
860
        // registries in reg.Namespace, then check their targets.
861
        grantList := &solarv1alpha1.ReferenceGrantList{}
10✔
862
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
10✔
863
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
864
                return requests
×
865
        }
×
866

867
        for i := range grantList.Items {
10✔
868
                grant := &grantList.Items[i]
×
869
                if !grantsRegistryResource(grant) {
×
870
                        continue
×
871
                }
872
                for _, from := range grant.Spec.From {
×
873
                        if from.Kind != "Target" || from.Group != solarGroup {
×
874
                                continue
×
875
                        }
876
                        crossTargets := &solarv1alpha1.TargetList{}
×
877
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
878
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
879
                                continue
×
880
                        }
881
                        for _, t := range crossTargets.Items {
×
882
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
883
                                        requests = append(requests, reconcile.Request{
×
884
                                                NamespacedName: types.NamespacedName{
×
885
                                                        Name:      t.Name,
×
886
                                                        Namespace: t.Namespace,
×
887
                                                },
×
888
                                        })
×
889
                                }
×
890
                        }
891
                }
892
        }
893

894
        return requests
10✔
895
}
896

897
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
898
// either because the grant controls Registry access (Target → Registry) or because
899
// it controls ComponentVersion access (Release → ComponentVersion, Releases live in
900
// the same namespace as their Targets).
901
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
8✔
902
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
8✔
903
        if !ok {
8✔
904
                return nil
×
905
        }
×
906

907
        var requests []reconcile.Request
8✔
908

8✔
909
        if grantsRegistryResource(grant) {
8✔
910
                for _, from := range grant.Spec.From {
×
911
                        if from.Kind != "Target" || from.Group != solarGroup {
×
912
                                continue
×
913
                        }
914
                        targets := &solarv1alpha1.TargetList{}
×
915
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
916
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
917
                                continue
×
918
                        }
919
                        for _, t := range targets.Items {
×
920
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
921
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
922
                                        requests = append(requests, reconcile.Request{
×
923
                                                NamespacedName: types.NamespacedName{
×
924
                                                        Name:      t.Name,
×
925
                                                        Namespace: t.Namespace,
×
926
                                                },
×
927
                                        })
×
928
                                }
×
929
                        }
930
                }
931
        }
932

933
        if grantsComponentVersionResource(grant) {
12✔
934
                for _, from := range grant.Spec.From {
8✔
935
                        if from.Kind != "Release" || from.Group != solarGroup {
4✔
936
                                continue
×
937
                        }
938
                        // Releases and Targets are co-located: list Targets in the Release's namespace.
939
                        targets := &solarv1alpha1.TargetList{}
4✔
940
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
941
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ComponentVersion grant mapping", "namespace", from.Namespace)
×
942
                                continue
×
943
                        }
944
                        for _, t := range targets.Items {
4✔
945
                                requests = append(requests, reconcile.Request{
×
946
                                        NamespacedName: types.NamespacedName{
×
947
                                                Name:      t.Name,
×
948
                                                Namespace: t.Namespace,
×
949
                                        },
×
950
                                })
×
951
                        }
×
952
                }
953
        }
954

955
        return requests
8✔
956
}
957

958
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
959
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
8✔
960
        for _, t := range grant.Spec.To {
16✔
961
                if t.Kind == "Registry" && t.Group == solarGroup {
8✔
962
                        return true
×
963
                }
×
964
        }
965

966
        return false
8✔
967
}
968

969
// mapReleaseToTargets maps a Release event to reconcile requests for all
970
// Targets that are bound to the release via ReleaseBindings.
971
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
65✔
972
        rel, ok := obj.(*solarv1alpha1.Release)
65✔
973
        if !ok {
65✔
974
                return nil
×
975
        }
×
976

977
        bindingList := &solarv1alpha1.ReleaseBindingList{}
65✔
978
        if err := r.List(ctx, bindingList,
65✔
979
                client.InNamespace(rel.Namespace),
65✔
980
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
65✔
981
        ); err != nil {
65✔
982
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
983

×
984
                return nil
×
985
        }
×
986

987
        seen := map[string]struct{}{}
65✔
988
        var requests []reconcile.Request
65✔
989

65✔
990
        for _, rb := range bindingList.Items {
65✔
991
                targetName := rb.Spec.TargetRef.Name
×
992
                if _, ok := seen[targetName]; ok {
×
993
                        continue
×
994
                }
995

996
                seen[targetName] = struct{}{}
×
997
                requests = append(requests, reconcile.Request{
×
998
                        NamespacedName: types.NamespacedName{
×
999
                                Name:      targetName,
×
1000
                                Namespace: rb.Namespace,
×
1001
                        },
×
1002
                })
×
1003
        }
1004

1005
        return requests
65✔
1006
}
1007

1008
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
23✔
1009
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
23✔
1010
        if !ok || rb.Spec.TargetRef.Name == "" {
23✔
1011
                return nil
×
1012
        }
×
1013

1014
        return []reconcile.Request{
23✔
1015
                {
23✔
1016
                        NamespacedName: types.NamespacedName{
23✔
1017
                                Name:      rb.Spec.TargetRef.Name,
23✔
1018
                                Namespace: rb.Namespace,
23✔
1019
                        },
23✔
1020
                },
23✔
1021
        }
23✔
1022
}
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