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

opendefensecloud / solution-arsenal / 27348406272

11 Jun 2026 12:57PM UTC coverage: 74.122% (+0.008%) from 74.114%
27348406272

push

github

web-flow
fix(controller): silently dropped cross-namespace releases in bootstrap chart (#570)

## What
Fix a bug where two same-named Releases from different namespaces
silently overwrite each other in the bootstrap chart.

## Why
`buildBootstrapInput` keyed the bootstrap `Releases` map on `ri.name` —
the bare Kubernetes Release object name. With cross-namespace
ReleaseBindings (#541) two Releases named `my-release` from different
namespaces can both survive conflict resolution and target the same
Target, colliding on the map key and dropping one deployment silently.

Fix: key on `uniqueName` instead — already guaranteed unique among
accepted releases by the resolver. Also extract the derivation rule into
a shared `effectiveUniqueName()` helper (was duplicated in both
controllers), and add a guard that returns an explicit error if
`uniqueName` is empty.

## Testing
- New `buildBootstrapInput` tests: cross-namespace collision (via full
`resolveReleaseConflicts` path) and explicit error on empty
`uniqueName`.
- `make test` / `make lint` — clean.

## Notes for reviewers
- **Upgrade path:** bootstrap map keys change from the Release object
name to `uniqueName`. Existing Targets where the two differ will have
inner FluxCD HelmReleases recreated on next bootstrap render.
- `effectiveUniqueName(rel, cv)` lives in `helpers.go`; both controllers
use it.
- Docs updated: `rendering-pipeline.md` and ADR-004 now document why
`uniqueName` is the bootstrap identity and why the object name is
unsuitable.

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

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

* **Bug Fixes**
* Prevented release overwrites when releases share the same Kubernetes
object name across namespaces by keying bootstrap inputs by a stable
... (continued)

16 of 16 new or added lines in 3 files covered. (100.0%)

6 existing lines in 2 files now uncovered.

2638 of 3559 relevant lines covered (74.12%)

31.99 hits per line

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

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

4
package controller
5

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

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

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

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

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

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

46
type releaseInfo struct {
47
        // bindingKey is "<namespace>/<name>" of the originating ReleaseBinding, used as a
48
        // deterministic tiebreaker when two releases share the same priority.
49
        bindingKey string
50
        name       string
51
        // uniqueName is the deduplication key computed by resolveReleaseConflicts:
52
        // Spec.UniqueName when set, otherwise the parent Component name from the CV.
53
        // It is guaranteed unique across all surviving releases and used as the
54
        // bootstrap map key to avoid collisions between same-named cross-namespace releases.
55
        uniqueName string
56
        release    *solarv1alpha1.Release
57
        cv         *solarv1alpha1.ComponentVersion
58
        rtName     string
59
        chartURL   string
60
}
61

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

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

89
// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release
90
// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask.
91
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
235✔
92
        log := ctrl.LoggerFrom(ctx)
235✔
93

235✔
94
        log.V(1).Info("Target is being reconciled", "req", req)
235✔
95

235✔
96
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
315✔
97
                return ctrl.Result{}, nil
80✔
98
        }
80✔
99

100
        // Fetch target
101
        target := &solarv1alpha1.Target{}
155✔
102
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
159✔
103
                if apierrors.IsNotFound(err) {
8✔
104
                        return ctrl.Result{}, nil
4✔
105
                }
4✔
106

107
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
108
        }
109

110
        // Handle deletion
111
        if !target.DeletionTimestamp.IsZero() {
152✔
112
                log.V(1).Info("Target is being deleted")
1✔
113
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
1✔
114

1✔
115
                // Delete owned RenderTasks
1✔
116
                if err := r.deleteOwnedRenderTasks(ctx, target); err != nil {
1✔
117
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
118
                }
×
119

120
                // Remove finalizer
121
                if slices.Contains(target.Finalizers, targetFinalizer) {
2✔
122
                        latest := &solarv1alpha1.Target{}
1✔
123
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
1✔
124
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
125
                        }
×
126

127
                        original := latest.DeepCopy()
1✔
128
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
2✔
129
                                return s == targetFinalizer
1✔
130
                        })
1✔
131
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
1✔
132
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
133
                        }
×
134
                }
135

136
                return ctrl.Result{}, nil
1✔
137
        }
138

139
        // Set finalizer if not set
140
        if !slices.Contains(target.Finalizers, targetFinalizer) {
179✔
141
                latest := &solarv1alpha1.Target{}
29✔
142
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
29✔
143
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
144
                }
×
145

146
                original := latest.DeepCopy()
29✔
147
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
29✔
148
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
29✔
149
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
150
                }
×
151

152
                return ctrl.Result{}, nil
29✔
153
        }
154

155
        // Resolve render registry — supports cross-namespace via ReferenceGrant
156
        registryNamespace := target.Namespace
121✔
157
        if target.Spec.RenderRegistryNamespace != "" {
121✔
158
                registryNamespace = target.Spec.RenderRegistryNamespace
×
159
        }
×
160

161
        // If the registry lives in a different namespace, verify a ReferenceGrant permits it
162
        // before attempting to fetch the object.
163
        if registryNamespace != target.Namespace {
121✔
164
                granted, err := r.registryGranted(ctx, registryNamespace, target.Namespace)
×
165
                if err != nil {
×
166
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for Registry")
×
167
                }
×
168
                if !granted {
×
169
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotGranted",
×
170
                                "No ReferenceGrant allows access to Registry "+target.Spec.RenderRegistryRef.Name+" in namespace "+registryNamespace); condErr != nil {
×
171
                                return ctrl.Result{}, condErr
×
172
                        }
×
173

174
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
175
                }
176
        }
177

178
        registry := &solarv1alpha1.Registry{}
121✔
179
        if err := r.Get(ctx, client.ObjectKey{
121✔
180
                Name:      target.Spec.RenderRegistryRef.Name,
121✔
181
                Namespace: registryNamespace,
121✔
182
        }, registry); err != nil {
140✔
183
                if apierrors.IsNotFound(err) {
38✔
184
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
19✔
185
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
19✔
186
                                return ctrl.Result{}, condErr
×
187
                        }
×
188

189
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
19✔
190
                }
191

192
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
193
        }
194

195
        if registry.Spec.SolarSecretRef == nil {
104✔
196
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
197
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
198
                        return ctrl.Result{}, condErr
×
199
                }
×
200

201
                return ctrl.Result{}, nil
2✔
202
        }
203

204
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
100✔
205
                "Registry resolved: "+registry.Name); condErr != nil {
100✔
206
                return ctrl.Result{}, condErr
×
207
        }
×
208

209
        // Build hostname→targetPullSecretName lookup from RegistryBindings for this target.
210
        pullSecretsByHost, err := r.buildPullSecretsLookup(ctx, target)
100✔
211
        if err != nil {
122✔
212
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "RegistryBindingConflict",
22✔
213
                        err.Error()); condErr != nil {
22✔
214
                        return ctrl.Result{}, condErr
×
215
                }
×
216

217
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to build pull secrets lookup from RegistryBindings")
22✔
218
        }
219

220
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
221
        // Filter on targetNamespace="" to exclude cross-namespace bindings (targetNamespace set) that share the
222
        // target name but point to a target in a different namespace.
223
        bindingList := &solarv1alpha1.ReleaseBindingList{}
78✔
224
        if err := r.List(ctx, bindingList,
78✔
225
                client.InNamespace(target.Namespace),
78✔
226
                client.MatchingFields{
78✔
227
                        indexReleaseBindingTargetName:      target.Name,
78✔
228
                        indexReleaseBindingTargetNamespace: "",
78✔
229
                },
78✔
230
        ); err != nil {
78✔
231
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
232
        }
×
233

234
        // Collect cross-namespace ReleaseBindings authorized by ReferenceGrants in target's namespace.
235
        crossNsBindings, crossNsErr := r.collectCrossNamespaceReleaseBindings(ctx, target)
78✔
236
        if crossNsErr != nil {
78✔
237
                return ctrl.Result{}, errLogAndWrap(log, crossNsErr, "failed to collect cross-namespace ReleaseBindings")
×
238
        }
×
239
        bindingList.Items = append(bindingList.Items, crossNsBindings...)
78✔
240

78✔
241
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
78✔
242
        // RegistryBinding collection is wired into the rendering pipeline.
78✔
243

78✔
244
        if len(bindingList.Items) == 0 {
88✔
245
                log.V(1).Info("No ReleaseBindings found for target")
10✔
246
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
10✔
247
                        "No ReleaseBindings found for this target"); condErr != nil {
10✔
248
                        return ctrl.Result{}, condErr
×
249
                }
×
250

251
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
10✔
252
                        "No ReleaseBindings found for this target"); condErr != nil {
10✔
253
                        return ctrl.Result{}, condErr
×
254
                }
×
255

256
                return ctrl.Result{}, nil
10✔
257
        }
258

259
        // For each bound release, ensure a per-release RenderTask exists
260
        var releases []releaseInfo
68✔
261

68✔
262
        pendingDeps := false
68✔
263

68✔
264
        for _, binding := range bindingList.Items {
155✔
265
                rel := &solarv1alpha1.Release{}
87✔
266
                if err := r.Get(ctx, client.ObjectKey{
87✔
267
                        Name:      binding.Spec.ReleaseRef.Name,
87✔
268
                        Namespace: binding.Namespace,
87✔
269
                }, rel); err != nil {
87✔
270
                        if apierrors.IsNotFound(err) {
×
271
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
272
                                pendingDeps = true
×
273

×
274
                                continue
×
275
                        }
276

277
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
278
                }
279

280
                cv := &solarv1alpha1.ComponentVersion{}
87✔
281
                cvNamespace := rel.Namespace
87✔
282
                if rel.Spec.ComponentVersionNamespace != "" {
87✔
283
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
284
                }
×
285

286
                if cvNamespace != rel.Namespace {
87✔
287
                        granted := false
×
288
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
289
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
290
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
291
                        }
×
292
                        for i := range grantList.Items {
×
293
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
294
                                        granted = true
×
295
                                        break
×
296
                                }
297
                        }
298
                        if !granted {
×
299
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
300
                                pendingDeps = true
×
301

×
302
                                continue
×
303
                        }
304
                }
305

306
                if err := r.Get(ctx, client.ObjectKey{
87✔
307
                        Name:      rel.Spec.ComponentVersionRef.Name,
87✔
308
                        Namespace: cvNamespace,
87✔
309
                }, cv); err != nil {
87✔
310
                        if apierrors.IsNotFound(err) {
×
311
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
312
                                pendingDeps = true
×
313

×
314
                                continue
×
315
                        }
316

317
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
318
                }
319

320
                rtName := releaseRenderTaskName(rel.Namespace, rel.Name, target.Name, rel.GetGeneration())
87✔
321
                releases = append(releases, releaseInfo{
87✔
322
                        bindingKey: binding.Namespace + "/" + binding.Name,
87✔
323
                        name:       rel.Name,
87✔
324
                        release:    rel,
87✔
325
                        cv:         cv,
87✔
326
                        rtName:     rtName,
87✔
327
                })
87✔
328
        }
329

330
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
331
        var skipped []string
68✔
332
        releases, skipped = resolveReleaseConflicts(releases)
68✔
333
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
68✔
334
                return ctrl.Result{}, condErr
×
335
        }
×
336

337
        if len(releases) == 0 && !pendingDeps {
68✔
338
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
339
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
340
                        return ctrl.Result{}, condErr
×
341
                }
×
342

343
                return ctrl.Result{}, nil
×
344
        }
345

346
        // Create per-release RenderTasks (one per target+release pair).
347
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
348
        allRendered := true
68✔
349

68✔
350
        for i, ri := range releases {
146✔
351
                rt := &solarv1alpha1.RenderTask{}
78✔
352
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
78✔
353

78✔
354
                switch {
78✔
355
                case apierrors.IsNotFound(err):
24✔
356
                        spec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
24✔
357
                        if specErr != nil {
34✔
358
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
10✔
359
                                        specErr.Error()); condErr != nil {
10✔
360
                                        return ctrl.Result{}, condErr
×
361
                                }
×
362

363
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
10✔
364
                        }
365

366
                        rt = &solarv1alpha1.RenderTask{
14✔
367
                                ObjectMeta: metav1.ObjectMeta{
14✔
368
                                        Name:      ri.rtName,
14✔
369
                                        Namespace: target.Namespace,
14✔
370
                                },
14✔
371
                                Spec: spec,
14✔
372
                        }
14✔
373

14✔
374
                        if err := r.Create(ctx, rt); err != nil {
14✔
375
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
376
                        }
×
377

378
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
14✔
379
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
14✔
380
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
14✔
381
                case err != nil:
×
382
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
383
                default:
54✔
384
                        // RenderTask exists — check for spec drift (e.g. pull secrets
54✔
385
                        // changed after a RegistryBinding was created/updated).
54✔
386
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
54✔
387
                        if specErr != nil {
54✔
388
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
×
389
                                        specErr.Error()); condErr != nil {
×
390
                                        return ctrl.Result{}, condErr
×
391
                                }
×
392

393
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
394
                        }
395

396
                        if !apiequality.Semantic.DeepEqual(rt.Spec, desiredSpec) {
56✔
397
                                if err := r.Delete(ctx, rt); err != nil {
2✔
398
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete stale release RenderTask")
×
399
                                }
×
400

401
                                rt = &solarv1alpha1.RenderTask{
2✔
402
                                        ObjectMeta: metav1.ObjectMeta{
2✔
403
                                                Name:      ri.rtName,
2✔
404
                                                Namespace: target.Namespace,
2✔
405
                                        },
2✔
406
                                        Spec: desiredSpec,
2✔
407
                                }
2✔
408

2✔
409
                                if err := r.Create(ctx, rt); err != nil {
2✔
410
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to recreate release RenderTask")
×
411
                                }
×
412

413
                                log.V(1).Info("Recreated release RenderTask (spec drift)", "release", ri.name, "renderTask", ri.rtName)
2✔
414
                                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update",
2✔
415
                                        "Recreated release RenderTask %s for release %s (spec drift)", ri.rtName, ri.name)
2✔
416
                        }
417
                }
418

419
                // Check if release RenderTask is complete
420
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
68✔
421
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
422
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
423
                                return ctrl.Result{}, condErr
×
424
                        }
×
425

426
                        return ctrl.Result{}, nil
×
427
                }
428

429
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
89✔
430
                        releases[i].chartURL = rt.Status.ChartURL
21✔
431
                } else {
68✔
432
                        allRendered = false
47✔
433
                }
47✔
434
        }
435

436
        if pendingDeps {
58✔
437
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
438
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
439
                        return ctrl.Result{}, condErr
×
440
                }
×
441

442
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
443
        }
444

445
        if !allRendered {
105✔
446
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
47✔
447
                        "Waiting for release RenderTasks to complete"); condErr != nil {
47✔
UNCOV
448
                        return ctrl.Result{}, condErr
×
UNCOV
449
                }
×
450

451
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
47✔
452
        }
453

454
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
11✔
455
                "All releases rendered successfully"); condErr != nil {
11✔
456
                return ctrl.Result{}, condErr
×
457
        }
×
458

459
        // Determine if a new bootstrap render is needed by checking whether the
460
        // current bootstrapVersion's RenderTask still matches the desired release set.
461
        bootstrapVersion := target.Status.BootstrapVersion
11✔
462
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
11✔
463
        bootstrapRT := &solarv1alpha1.RenderTask{}
11✔
464
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
11✔
465

11✔
466
        needsNewBootstrap := false
11✔
467

11✔
468
        switch {
11✔
469
        case apierrors.IsNotFound(err):
1✔
470
                // No RenderTask for the current version yet — create one
1✔
471
                needsNewBootstrap = true
1✔
472
        case err != nil:
×
473
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
474
        default:
10✔
475
                // RenderTask exists — check if the desired bootstrap input changed
10✔
476
                // (release set, resolved refs/tags, or userdata)
10✔
477
                desiredInput, inputErr := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
10✔
478
                if inputErr != nil {
10✔
479
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
480
                }
×
481

482
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
10✔
483
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
11✔
484
                        bootstrapVersion++
1✔
485
                        needsNewBootstrap = true
1✔
486
                }
1✔
487
        }
488

489
        if needsNewBootstrap {
13✔
490
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
491
                if specErr != nil {
2✔
492
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
493
                }
×
494

495
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
496
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
497
                        ObjectMeta: metav1.ObjectMeta{
2✔
498
                                Name:      bootstrapRTName,
2✔
499
                                Namespace: target.Namespace,
2✔
500
                        },
2✔
501
                        Spec: spec,
2✔
502
                }
2✔
503

2✔
504
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
505
                        if !apierrors.IsAlreadyExists(err) {
×
506
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
507
                        }
×
508

509
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
510
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
511
                        }
×
512
                } else {
2✔
513
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
514
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
515
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
516
                }
2✔
517

518
                // Persist the new bootstrapVersion in status
519
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
520
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
521
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
522
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
523
                        }
×
524
                }
525
        }
526

527
        // Update target status from bootstrap RenderTask
528
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
11✔
529
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
530
                        "Bootstrap rendering failed"); condErr != nil {
×
531
                        return ctrl.Result{}, condErr
×
532
                }
×
533

534
                return ctrl.Result{}, nil
×
535
        }
536

537
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
16✔
538
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
5✔
539
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
5✔
540
                        return ctrl.Result{}, condErr
×
541
                }
×
542

543
                // Clean up stale RenderTasks owned by this target (old versions)
544
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
5✔
545
                for _, ri := range releases {
13✔
546
                        currentRTNames[ri.rtName] = struct{}{}
8✔
547
                }
8✔
548
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
5✔
549
                        log.Error(err, "failed to clean up stale RenderTasks")
×
550
                }
×
551

552
                return ctrl.Result{}, nil
5✔
553
        }
554

555
        // Still running
556
        return ctrl.Result{}, nil
6✔
557
}
558

559
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
304✔
560
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
304✔
561
                Type:               condType,
304✔
562
                Status:             status,
304✔
563
                ObservedGeneration: target.Generation,
304✔
564
                Reason:             reason,
304✔
565
                Message:            message,
304✔
566
        })
304✔
567
        if changed {
384✔
568
                if err := r.Status().Update(ctx, target); err != nil {
80✔
UNCOV
569
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
×
UNCOV
570
                }
×
571
        }
572

573
        return nil
304✔
574
}
575

576
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
68✔
577
        if len(skipped) == 0 {
127✔
578
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
59✔
579
        }
59✔
580

581
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
582
}
583

584
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
585
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
586
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
587
// It returns the accepted releases and a slice of human-readable filter messages.
588
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
77✔
589
        if len(releases) == 0 {
78✔
590
                return releases, nil
1✔
591
        }
1✔
592

593
        // Step A: uniqueName deduplication.
594
        // When UniqueName is empty, fall back to the parent Component name from the CV.
595
        namedGroups := map[string][]releaseInfo{}
76✔
596

76✔
597
        for i, ri := range releases {
177✔
598
                uname := effectiveUniqueName(ri.release, ri.cv)
101✔
599
                releases[i].uniqueName = uname
101✔
600
                namedGroups[uname] = append(namedGroups[uname], releases[i])
101✔
601
        }
101✔
602

603
        var accepted []releaseInfo
76✔
604

76✔
605
        var skipped []string
76✔
606

76✔
607
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
76✔
608
        byPriority := func(a, b releaseInfo) bool {
101✔
609
                if a.release.Spec.Priority != b.release.Spec.Priority {
35✔
610
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
611
                }
10✔
612

613
                return a.bindingKey < b.bindingKey
15✔
614
        }
615

616
        uniqueNames := make([]string, 0, len(namedGroups))
76✔
617
        for k := range namedGroups {
168✔
618
                uniqueNames = append(uniqueNames, k)
92✔
619
        }
92✔
620

621
        sort.Strings(uniqueNames)
76✔
622

76✔
623
        for _, uniqueName := range uniqueNames {
168✔
624
                group := namedGroups[uniqueName]
92✔
625
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
101✔
626

627
                accepted = append(accepted, group[0])
92✔
628

92✔
629
                for _, loser := range group[1:] {
101✔
630
                        skipped = append(skipped, fmt.Sprintf(
9✔
631
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
632
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
633
                        ))
9✔
634
                }
9✔
635
        }
636

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

642
        resolved := make([]releaseInfo, 0, len(accepted))
76✔
643

76✔
644
        for _, ri := range accepted {
168✔
645
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
92✔
646
                var riSelector labels.Selector
92✔
647
                if ri.release.Spec.AntiAffinity != nil {
99✔
648
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
649
                        if err != nil {
8✔
650
                                skipped = append(skipped, fmt.Sprintf(
1✔
651
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
652
                                        ri.bindingKey, err,
1✔
653
                                ))
1✔
654

1✔
655
                                continue
1✔
656
                        }
657

658
                        riSelector = sel
6✔
659
                }
660

661
                // Check both directions: ri's anti-affinity against already-resolved labels,
662
                // and already-resolved anti-affinities against ri's labels.
663
                conflict := ""
91✔
664
                for _, other := range resolved {
107✔
665
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
20✔
666
                                conflict = other.bindingKey
4✔
667
                                break
4✔
668
                        }
669

670
                        if other.release.Spec.AntiAffinity != nil {
13✔
671
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
672
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
673
                                        conflict = other.bindingKey
1✔
674
                                        break
1✔
675
                                }
676
                        }
677
                }
678

679
                if conflict != "" {
96✔
680
                        skipped = append(skipped, fmt.Sprintf(
5✔
681
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
682
                                ri.bindingKey, conflict,
5✔
683
                        ))
5✔
684
                } else {
91✔
685
                        resolved = append(resolved, ri)
86✔
686
                }
86✔
687
        }
688

689
        return resolved, skipped
76✔
690
}
691

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

5✔
698
        rtList := &solarv1alpha1.RenderTaskList{}
5✔
699
        if err := r.List(ctx, rtList,
5✔
700
                client.InNamespace(target.Namespace),
5✔
701
                client.MatchingFields{indexOwnerKind: "Target"},
5✔
702
        ); err != nil {
5✔
703
                return err
×
704
        }
×
705

706
        for i := range rtList.Items {
19✔
707
                rt := &rtList.Items[i]
14✔
708
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
14✔
709
                        continue
×
710
                }
711

712
                if _, current := currentRTNames[rt.Name]; current {
27✔
713
                        continue
13✔
714
                }
715

716
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
717
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
718
                        return err
×
719
                }
×
720

721
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
722
                        "Deleted stale RenderTask %s", rt.Name)
1✔
723
        }
724

725
        return nil
5✔
726
}
727

728
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
729
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
730
        if err := r.List(ctx, rtList,
1✔
731
                client.InNamespace(target.Namespace),
1✔
732
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
733
        ); err != nil {
1✔
734
                return err
×
735
        }
×
736

737
        for i := range rtList.Items {
1✔
738
                rt := &rtList.Items[i]
×
739
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
740
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
741
                                return err
×
742
                        }
×
743
                }
744
        }
745

746
        return nil
1✔
747
}
748

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

78✔
753
        var targetNamespace string
78✔
754
        if rel.Spec.TargetNamespace != nil {
156✔
755
                targetNamespace = *rel.Spec.TargetNamespace
78✔
756
        }
78✔
757

758
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
78✔
759
        if err != nil {
88✔
760
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
761
        }
10✔
762

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

68✔
769
        return solarv1alpha1.RenderTaskSpec{
68✔
770
                RendererConfig: solarv1alpha1.RendererConfig{
68✔
771
                        Type: solarv1alpha1.RendererConfigTypeRelease,
68✔
772
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
68✔
773
                                Chart: solarv1alpha1.ChartConfig{
68✔
774
                                        Name:        chartName,
68✔
775
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
68✔
776
                                        Version:     tag,
68✔
777
                                        AppVersion:  tag,
68✔
778
                                },
68✔
779
                                Input: solarv1alpha1.ReleaseInput{
68✔
780
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
68✔
781
                                        Resources:  resolvedResources,
68✔
782
                                        Entrypoint: cv.Spec.Entrypoint,
68✔
783
                                },
68✔
784
                                Values:          rel.Spec.Values,
68✔
785
                                TargetNamespace: targetNamespace,
68✔
786
                        },
68✔
787
                },
68✔
788
                Repository:     repo,
68✔
789
                Tag:            tag,
68✔
790
                BaseURL:        registry.Spec.Hostname,
68✔
791
                PushSecretRef:  registry.Spec.SolarSecretRef,
68✔
792
                FailedJobTTL:   rel.Spec.FailedJobTTL,
68✔
793
                OwnerName:      target.Name,
68✔
794
                OwnerNamespace: target.Namespace,
68✔
795
                OwnerKind:      "Target",
68✔
796
        }, nil
68✔
797
}
798

799
// buildBootstrapInput constructs the desired BootstrapInput from the current
800
// target and resolved releases. Used for both comparison and spec construction.
801
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string) (solarv1alpha1.BootstrapInput, error) {
14✔
802
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
14✔
803

14✔
804
        for _, ri := range releases {
36✔
805
                if ri.uniqueName == "" {
23✔
806
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
807
                }
1✔
808

809
                ref, err := ociname.ParseReference(ri.chartURL)
21✔
810
                if err != nil {
21✔
811
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
812
                }
×
813

814
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
21✔
815
                if err != nil {
21✔
816
                        return solarv1alpha1.BootstrapInput{}, err
×
817
                }
×
818

819
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
21✔
820
                        Repository:     strings.TrimPrefix(repo, "oci://"),
21✔
821
                        Tag:            ref.Identifier(),
21✔
822
                        PullSecretName: renderRegistryPullSecret,
21✔
823
                }
21✔
824
        }
825

826
        return solarv1alpha1.BootstrapInput{
13✔
827
                Releases: resolvedReleases,
13✔
828
                Userdata: target.Spec.Userdata,
13✔
829
        }, nil
13✔
830
}
831

832
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
2✔
833
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
2✔
834
        if err != nil {
2✔
835
                return solarv1alpha1.RenderTaskSpec{}, err
×
836
        }
×
837

838
        releaseNames := make([]string, 0, len(releases))
2✔
839
        for _, ri := range releases {
5✔
840
                releaseNames = append(releaseNames, ri.name)
3✔
841
        }
3✔
842

843
        sort.Strings(releaseNames)
2✔
844

2✔
845
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
2✔
846
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
2✔
847
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
2✔
848

2✔
849
        return solarv1alpha1.RenderTaskSpec{
2✔
850
                RendererConfig: solarv1alpha1.RendererConfig{
2✔
851
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
2✔
852
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
2✔
853
                                Chart: solarv1alpha1.ChartConfig{
2✔
854
                                        Name:        chartName,
2✔
855
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
2✔
856
                                        Version:     tag,
2✔
857
                                        AppVersion:  tag,
2✔
858
                                },
2✔
859
                                Input: input,
2✔
860
                        },
2✔
861
                },
2✔
862
                Repository:     repo,
2✔
863
                Tag:            tag,
2✔
864
                BaseURL:        registry.Spec.Hostname,
2✔
865
                PushSecretRef:  registry.Spec.SolarSecretRef,
2✔
866
                OwnerName:      target.Name,
2✔
867
                OwnerNamespace: target.Namespace,
2✔
868
                OwnerKind:      "Target",
2✔
869
        }, nil
2✔
870
}
871

872
// SetupWithManager sets up the controller with the Manager.
873
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
874
        return ctrl.NewControllerManagedBy(mgr).
1✔
875
                For(&solarv1alpha1.Target{}).
1✔
876
                Watches(
1✔
877
                        &solarv1alpha1.ReleaseBinding{},
1✔
878
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
879
                ).
1✔
880
                Watches(
1✔
881
                        &solarv1alpha1.RenderTask{},
1✔
882
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
883
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
884
                ).
1✔
885
                Watches(
1✔
886
                        &solarv1alpha1.Registry{},
1✔
887
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
888
                ).
1✔
889
                Watches(
1✔
890
                        &solarv1alpha1.RegistryBinding{},
1✔
891
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
892
                ).
1✔
893
                Watches(
1✔
894
                        &solarv1alpha1.ReferenceGrant{},
1✔
895
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
896
                ).
1✔
897
                Watches(
1✔
898
                        &solarv1alpha1.Release{},
1✔
899
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
900
                ).
1✔
901
                Complete(r)
1✔
902
}
1✔
903

904
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
905
// fromNamespace to reference the named registry.
906
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
907
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
908
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
909
                return false, err
×
910
        }
×
911
        for i := range grantList.Items {
×
912
                grant := &grantList.Items[i]
×
913
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
914
                        return true, nil
×
915
                }
×
916
        }
917

918
        return false, nil
×
919
}
920

921
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
922
// fromNamespace to reference Registry resources in the grant's namespace.
923
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
924
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
925
}
×
926

927
// mapRegistryToTargets maps a Registry event to reconcile requests for all
928
// Targets that reference it — either in the same namespace or cross-namespace.
929
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
27✔
930
        reg, ok := obj.(*solarv1alpha1.Registry)
27✔
931
        if !ok {
27✔
932
                return nil
×
933
        }
×
934

935
        // Same-namespace targets
936
        targetList := &solarv1alpha1.TargetList{}
27✔
937
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
27✔
938
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
939

×
940
                return nil
×
941
        }
×
942

943
        var requests []reconcile.Request
27✔
944
        for _, t := range targetList.Items {
28✔
945
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
1✔
946
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
1✔
947
                        requests = append(requests, reconcile.Request{
×
948
                                NamespacedName: types.NamespacedName{
×
949
                                        Name:      t.Name,
×
950
                                        Namespace: t.Namespace,
×
951
                                },
×
952
                        })
×
953
                }
×
954
        }
955

956
        // Cross-namespace targets: find namespaces that have been granted access to
957
        // registries in reg.Namespace, then check their targets.
958
        grantList := &solarv1alpha1.ReferenceGrantList{}
27✔
959
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
27✔
960
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
961
                return requests
×
962
        }
×
963

964
        for i := range grantList.Items {
27✔
965
                grant := &grantList.Items[i]
×
966
                if !grantsRegistryResource(grant) {
×
967
                        continue
×
968
                }
969
                for _, from := range grant.Spec.From {
×
970
                        if from.Kind != "Target" || from.Group != solarGroup {
×
971
                                continue
×
972
                        }
973
                        crossTargets := &solarv1alpha1.TargetList{}
×
974
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
975
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
976
                                continue
×
977
                        }
978
                        for _, t := range crossTargets.Items {
×
979
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
980
                                        requests = append(requests, reconcile.Request{
×
981
                                                NamespacedName: types.NamespacedName{
×
982
                                                        Name:      t.Name,
×
983
                                                        Namespace: t.Namespace,
×
984
                                                },
×
985
                                        })
×
986
                                }
×
987
                        }
988
                }
989
        }
990

991
        return requests
27✔
992
}
993

994
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
995
// each bound Registry, and returns a map from registry hostname to
996
// targetPullSecretName. Registries without a targetPullSecretName are included
997
// with an empty string (anonymous pull).
998
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
100✔
999
        rbList := &solarv1alpha1.RegistryBindingList{}
100✔
1000
        if err := r.List(ctx, rbList,
100✔
1001
                client.InNamespace(target.Namespace),
100✔
1002
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
100✔
1003
        ); err != nil {
100✔
1004
                return nil, err
×
1005
        }
×
1006

1007
        type hostEntry struct {
100✔
1008
                pullSecret  string
100✔
1009
                bindingName string
100✔
1010
        }
100✔
1011

100✔
1012
        lookup := make(map[string]hostEntry, len(rbList.Items))
100✔
1013

100✔
1014
        for _, rb := range rbList.Items {
142✔
1015
                reg := &solarv1alpha1.Registry{}
42✔
1016
                if err := r.Get(ctx, client.ObjectKey{
42✔
1017
                        Name:      rb.Spec.RegistryRef.Name,
42✔
1018
                        Namespace: rb.Namespace,
42✔
1019
                }, reg); err != nil {
52✔
1020
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1021
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1022
                }
10✔
1023

1024
                host := strings.ToLower(reg.Spec.Hostname)
32✔
1025
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
44✔
1026
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
12✔
1027
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
12✔
1028
                }
12✔
1029

1030
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
20✔
1031
        }
1032

1033
        result := make(map[string]string, len(lookup))
78✔
1034
        for host, entry := range lookup {
86✔
1035
                result[host] = entry.pullSecret
8✔
1036
        }
8✔
1037

1038
        return result, nil
78✔
1039
}
1040

1041
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1042
// for the referenced Target.
1043
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
7✔
1044
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
7✔
1045
        if !ok {
7✔
1046
                return nil
×
1047
        }
×
1048

1049
        if rb.Spec.TargetRef.Name == "" {
7✔
1050
                return nil
×
1051
        }
×
1052

1053
        return []reconcile.Request{
7✔
1054
                {
7✔
1055
                        NamespacedName: types.NamespacedName{
7✔
1056
                                Name:      rb.Spec.TargetRef.Name,
7✔
1057
                                Namespace: rb.Namespace,
7✔
1058
                        },
7✔
1059
                },
7✔
1060
        }
7✔
1061
}
1062

1063
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1064
// either because the grant controls Registry access (Target → Registry) or because
1065
// it controls ComponentVersion access (Release → ComponentVersion).
1066
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
13✔
1067
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
13✔
1068
        if !ok {
13✔
1069
                return nil
×
1070
        }
×
1071

1072
        var requests []reconcile.Request
13✔
1073

13✔
1074
        if grantsRegistryResource(grant) {
13✔
1075
                for _, from := range grant.Spec.From {
×
1076
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1077
                                continue
×
1078
                        }
1079
                        targets := &solarv1alpha1.TargetList{}
×
1080
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
1081
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1082
                                continue
×
1083
                        }
1084
                        for _, t := range targets.Items {
×
1085
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
1086
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
1087
                                        requests = append(requests, reconcile.Request{
×
1088
                                                NamespacedName: types.NamespacedName{
×
1089
                                                        Name:      t.Name,
×
1090
                                                        Namespace: t.Namespace,
×
1091
                                                },
×
1092
                                        })
×
1093
                                }
×
1094
                        }
1095
                }
1096
        }
1097

1098
        if grantsComponentVersionResource(grant) {
18✔
1099
                seen := map[string]struct{}{}
5✔
1100
                for _, from := range grant.Spec.From {
10✔
1101
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
1102
                                continue
×
1103
                        }
1104
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
1105
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
1106
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1107
                                continue
×
1108
                        }
1109
                        for _, rb := range bindings.Items {
6✔
1110
                                if rb.Spec.TargetRef.Name == "" {
1✔
1111
                                        continue
×
1112
                                }
1113
                                targetNs := rb.Namespace
1✔
1114
                                if rb.Spec.TargetNamespace != "" {
2✔
1115
                                        targetNs = rb.Spec.TargetNamespace
1✔
1116
                                }
1✔
1117
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1118
                                if _, ok := seen[key]; ok {
1✔
1119
                                        continue
×
1120
                                }
1121
                                seen[key] = struct{}{}
1✔
1122
                                requests = append(requests, reconcile.Request{
1✔
1123
                                        NamespacedName: types.NamespacedName{
1✔
1124
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1125
                                                Namespace: targetNs,
1✔
1126
                                        },
1✔
1127
                                })
1✔
1128
                        }
1129
                }
1130
        }
1131

1132
        if grantsReleaseBindingToTargetResource(grant) {
17✔
1133
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1134
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1135
                // the new or removed cross-namespace ReleaseBindings.
4✔
1136
                targets := &solarv1alpha1.TargetList{}
4✔
1137
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1138
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1139
                } else {
4✔
1140
                        for _, t := range targets.Items {
8✔
1141
                                requests = append(requests, reconcile.Request{
4✔
1142
                                        NamespacedName: types.NamespacedName{
4✔
1143
                                                Name:      t.Name,
4✔
1144
                                                Namespace: t.Namespace,
4✔
1145
                                        },
4✔
1146
                                })
4✔
1147
                        }
4✔
1148
                }
1149
        }
1150

1151
        return requests
13✔
1152
}
1153

1154
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1155
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
13✔
1156
        for _, t := range grant.Spec.To {
26✔
1157
                if t.Kind == "Registry" && t.Group == solarGroup {
13✔
1158
                        return true
×
1159
                }
×
1160
        }
1161

1162
        return false
13✔
1163
}
1164

1165
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1166
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1167
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
25✔
1168
        hasReleaseBindingFrom := false
25✔
1169
        for _, f := range grant.Spec.From {
50✔
1170
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
41✔
1171
                        hasReleaseBindingFrom = true
16✔
1172
                        break
16✔
1173
                }
1174
        }
1175
        if !hasReleaseBindingFrom {
34✔
1176
                return false
9✔
1177
        }
9✔
1178
        for _, t := range grant.Spec.To {
32✔
1179
                if t.Kind == "Target" && t.Group == solarGroup {
32✔
1180
                        return true
16✔
1181
                }
16✔
1182
        }
1183

1184
        return false
×
1185
}
1186

1187
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1188
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1189
// a ReferenceGrant in target's namespace.
1190
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
78✔
1191
        grantList := &solarv1alpha1.ReferenceGrantList{}
78✔
1192
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
78✔
1193
                return nil, err
×
1194
        }
×
1195

1196
        seen := make(map[string]struct{})
78✔
1197
        var result []solarv1alpha1.ReleaseBinding
78✔
1198
        for i := range grantList.Items {
90✔
1199
                grant := &grantList.Items[i]
12✔
1200
                if !grantsReleaseBindingToTargetResource(grant) {
12✔
1201
                        continue
×
1202
                }
1203
                for _, from := range grant.Spec.From {
24✔
1204
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
12✔
1205
                                continue
×
1206
                        }
1207
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
12✔
1208
                        if err := r.List(ctx, crossBindings,
12✔
1209
                                client.InNamespace(from.Namespace),
12✔
1210
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
12✔
1211
                        ); err != nil {
12✔
1212
                                return nil, err
×
1213
                        }
×
1214
                        for _, rb := range crossBindings.Items {
21✔
1215
                                if rb.Spec.TargetNamespace != target.Namespace {
9✔
1216
                                        continue
×
1217
                                }
1218
                                key := rb.Namespace + "/" + rb.Name
9✔
1219
                                if _, exists := seen[key]; exists {
10✔
1220
                                        continue
1✔
1221
                                }
1222
                                seen[key] = struct{}{}
8✔
1223
                                result = append(result, rb)
8✔
1224
                        }
1225
                }
1226
        }
1227

1228
        return result, nil
78✔
1229
}
1230

1231
// mapReleaseToTargets maps a Release event to reconcile requests for all
1232
// Targets that are bound to the release via ReleaseBindings.
1233
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
90✔
1234
        rel, ok := obj.(*solarv1alpha1.Release)
90✔
1235
        if !ok {
90✔
1236
                return nil
×
1237
        }
×
1238

1239
        bindingList := &solarv1alpha1.ReleaseBindingList{}
90✔
1240
        if err := r.List(ctx, bindingList,
90✔
1241
                client.InNamespace(rel.Namespace),
90✔
1242
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
90✔
1243
        ); err != nil {
90✔
1244
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1245

×
1246
                return nil
×
1247
        }
×
1248

1249
        seen := map[string]struct{}{}
90✔
1250
        var requests []reconcile.Request
90✔
1251

90✔
1252
        for _, rb := range bindingList.Items {
92✔
1253
                targetNs := rb.Namespace
2✔
1254
                if rb.Spec.TargetNamespace != "" {
2✔
1255
                        targetNs = rb.Spec.TargetNamespace
×
1256
                }
×
1257

1258
                key := targetNs + "/" + rb.Spec.TargetRef.Name
2✔
1259
                if _, ok := seen[key]; ok {
2✔
1260
                        continue
×
1261
                }
1262

1263
                seen[key] = struct{}{}
2✔
1264
                requests = append(requests, reconcile.Request{
2✔
1265
                        NamespacedName: types.NamespacedName{
2✔
1266
                                Name:      rb.Spec.TargetRef.Name,
2✔
1267
                                Namespace: targetNs,
2✔
1268
                        },
2✔
1269
                })
2✔
1270
        }
1271

1272
        return requests
90✔
1273
}
1274

1275
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
33✔
1276
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
33✔
1277
        if !ok || rb.Spec.TargetRef.Name == "" {
33✔
1278
                return nil
×
1279
        }
×
1280

1281
        targetNs := rb.Namespace
33✔
1282
        if rb.Spec.TargetNamespace != "" {
42✔
1283
                targetNs = rb.Spec.TargetNamespace
9✔
1284
        }
9✔
1285

1286
        return []reconcile.Request{
33✔
1287
                {
33✔
1288
                        NamespacedName: types.NamespacedName{
33✔
1289
                                Name:      rb.Spec.TargetRef.Name,
33✔
1290
                                Namespace: targetNs,
33✔
1291
                        },
33✔
1292
                },
33✔
1293
        }
33✔
1294
}
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