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

opendefensecloud / solution-arsenal / 28087886196

24 Jun 2026 09:10AM UTC coverage: 75.543% (+0.08%) from 75.468%
28087886196

push

github

web-flow
fix(ci): make Makefile the source of truth for e2e K8s version (v1.36.1) (#648)

## What
Fix the E2E workflow by making the Makefile the single source of truth
for the K8s version: bump `ENVTEST_K8S_VERSION` to `1.36.1` and let
`make e2e-cluster` own Kind cluster creation instead of the workflow.

## Why
The workflow pre-created the `solar-test-e2e` cluster via
`helm/kind-action`, while `make test-e2e` (`make e2e-cluster` →
`ensure-kind-cluster.sh`) created its own cluster pinned to
`KIND_NODE_IMAGE` (derived from `ENVTEST_K8S_VERSION`). Two creators
with two version values meant they could disagree — and they did,
breaking E2E. The first attempted bump also exposed that
`kindest/node:v1.36.0` was never published; `v1.36.1` is the real tag.
Now the version lives only in the Makefile, `kind` is just a binary the
workflow installs, and `make e2e-cluster` is the sole creator (no-op
reuse when the cluster already matches).

## Testing
CI E2E run on this branch.

## Notes for reviewers
- `ENVTEST_K8S_VERSION 1.36.0 → 1.36.1`; `KIND_NODE_IMAGE` auto-derives
`kindest/node:v1.36.1`, and envtest fetches `1.36.1` via the sideload
script (dl.k8s.io on Linux CI).
- Removed the `helm/kind-action` step; `kind v0.31.0` is now installed
in the `Install Tools` step. Cluster creation is fully owned by `make
e2e-cluster` in the suite's `BeforeSuite`.
- Follow-up (separate issue/PR, not here): migrate all CI workflows to
run under nix so every tool version comes from the dev shell instead of
hardcoded installs.

## Checklist
- [x] ~~Tests added/updated~~ n/a
- [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

4037 of 5344 relevant lines covered (75.54%)

34.27 hits per line

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

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

4
package controller
5

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

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

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

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

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

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

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

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

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

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

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

482✔
102
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
698✔
103
                return ctrl.Result{}, nil
216✔
104
        }
216✔
105

106
        // Fetch target
107
        target := &solarv1alpha1.Target{}
266✔
108
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
316✔
109
                if apierrors.IsNotFound(err) {
100✔
110
                        return ctrl.Result{}, nil
50✔
111
                }
50✔
112

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

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

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

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

131
                // Remove protection finalizer from Registry if no other Target or RegistryBinding references it.
132
                registryNamespace := target.Namespace
2✔
133
                if target.Spec.RenderRegistryNamespace != "" {
2✔
134
                        registryNamespace = target.Spec.RenderRegistryNamespace
×
135
                }
×
136

137
                if target.Spec.RenderRegistryRef.Name != "" {
4✔
138
                        registry := &solarv1alpha1.Registry{}
2✔
139
                        if err := r.Get(ctx, client.ObjectKey{Name: target.Spec.RenderRegistryRef.Name, Namespace: registryNamespace}, registry); err != nil {
2✔
140
                                if !apierrors.IsNotFound(err) {
×
141
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry for finalizer cleanup")
×
142
                                }
×
143
                        } else if err := r.removeRegistryRefFinalizer(ctx, target, registry); err != nil {
2✔
144
                                return ctrl.Result{}, err
×
145
                        }
×
146
                }
147

148
                // Remove finalizer
149
                if slices.Contains(target.Finalizers, targetFinalizer) {
4✔
150
                        latest := &solarv1alpha1.Target{}
2✔
151
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
2✔
152
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
153
                        }
×
154

155
                        original := latest.DeepCopy()
2✔
156
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
4✔
157
                                return s == targetFinalizer
2✔
158
                        })
2✔
159
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
2✔
160
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
161
                        }
×
162
                }
163

164
                return ctrl.Result{}, nil
2✔
165
        }
166

167
        // Set finalizer if not set
168
        if !slices.Contains(target.Finalizers, targetFinalizer) {
250✔
169
                latest := &solarv1alpha1.Target{}
36✔
170
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
36✔
171
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
172
                }
×
173

174
                if !slices.Contains(latest.Finalizers, targetFinalizer) {
72✔
175
                        original := latest.DeepCopy()
36✔
176
                        latest.Finalizers = append(latest.Finalizers, targetFinalizer)
36✔
177
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
36✔
178
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
179
                        }
×
180
                }
181

182
                return ctrl.Result{}, nil
36✔
183
        }
184

185
        // Resolve render registry — supports cross-namespace via ReferenceGrant
186
        registryNamespace := target.Namespace
178✔
187
        if target.Spec.RenderRegistryNamespace != "" {
178✔
188
                registryNamespace = target.Spec.RenderRegistryNamespace
×
189
        }
×
190

191
        // If the registry lives in a different namespace, verify a ReferenceGrant permits it
192
        // before attempting to fetch the object.
193
        if registryNamespace != target.Namespace {
178✔
194
                granted, err := r.registryGranted(ctx, registryNamespace, target.Namespace)
×
195
                if err != nil {
×
196
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for Registry")
×
197
                }
×
198
                if !granted {
×
199
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotGranted",
×
200
                                "No ReferenceGrant allows access to Registry "+target.Spec.RenderRegistryRef.Name+" in namespace "+registryNamespace); condErr != nil {
×
201
                                return ctrl.Result{}, condErr
×
202
                        }
×
203

204
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
205
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
×
206
                }
207
        }
208

209
        registry := &solarv1alpha1.Registry{}
178✔
210
        if err := r.Get(ctx, client.ObjectKey{
178✔
211
                Name:      target.Spec.RenderRegistryRef.Name,
178✔
212
                Namespace: registryNamespace,
178✔
213
        }, registry); err != nil {
225✔
214
                if apierrors.IsNotFound(err) {
94✔
215
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
47✔
216
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
47✔
217
                                return ctrl.Result{}, condErr
×
218
                        }
×
219

220
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
47✔
221
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
47✔
222
                }
223

224
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
225
        }
226

227
        // Protect Registry from deletion while this Target references it, regardless of
228
        // whether SolarSecretRef is configured — the Target still references this Registry.
229
        if !slices.Contains(registry.Finalizers, registryRefFinalizer) {
156✔
230
                latest := registry.DeepCopy()
25✔
231
                latest.Finalizers = append(latest.Finalizers, registryRefFinalizer)
25✔
232
                if err := r.Patch(ctx, latest, client.MergeFromWithOptions(registry, client.MergeFromWithOptimisticLock{})); err != nil {
25✔
233
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add protection finalizer to Registry")
×
234
                }
×
235
        }
236

237
        if registry.Spec.SolarSecretRef == nil {
134✔
238
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
3✔
239
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
4✔
240
                        return ctrl.Result{}, condErr
1✔
241
                }
1✔
242

243
                return ctrl.Result{}, nil
2✔
244
        }
245

246
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
128✔
247
                "Registry resolved: "+registry.Name); condErr != nil {
128✔
248
                return ctrl.Result{}, condErr
×
249
        }
×
250

251
        // Build hostname→targetPullSecretName lookup from RegistryBindings for this target.
252
        pullSecretsByHost, err := r.buildPullSecretsLookup(ctx, target)
128✔
253
        if err != nil {
148✔
254
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "RegistryBindingConflict",
20✔
255
                        err.Error()); condErr != nil {
20✔
256
                        return ctrl.Result{}, condErr
×
257
                }
×
258

259
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to build pull secrets lookup from RegistryBindings")
20✔
260
        }
261

262
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
263
        allBindings := &solarv1alpha1.ReleaseBindingList{}
108✔
264
        if err := r.APIReader.List(ctx, allBindings, client.InNamespace(target.Namespace)); err != nil {
108✔
265
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
266
        }
×
267
        bindingList := &solarv1alpha1.ReleaseBindingList{}
108✔
268
        for _, rb := range allBindings.Items {
225✔
269
                if rb.Spec.TargetRef.Name == target.Name && rb.Spec.TargetNamespace == "" {
234✔
270
                        bindingList.Items = append(bindingList.Items, rb)
117✔
271
                }
117✔
272
        }
273

274
        // Collect cross-namespace ReleaseBindings authorized by ReferenceGrants in target's namespace.
275
        crossNsBindings, crossNsErr := r.collectCrossNamespaceReleaseBindings(ctx, target)
108✔
276
        if crossNsErr != nil {
108✔
277
                return ctrl.Result{}, errLogAndWrap(log, crossNsErr, "failed to collect cross-namespace ReleaseBindings")
×
278
        }
×
279
        bindingList.Items = append(bindingList.Items, crossNsBindings...)
108✔
280

108✔
281
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
108✔
282
        // RegistryBinding collection is wired into the rendering pipeline.
108✔
283

108✔
284
        if len(bindingList.Items) == 0 {
116✔
285
                log.V(1).Info("No ReleaseBindings found for target")
8✔
286
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
8✔
287
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
288
                        return ctrl.Result{}, condErr
×
289
                }
×
290

291
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
8✔
292
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
293
                        return ctrl.Result{}, condErr
×
294
                }
×
295

296
                // Clean up any stale RenderTasks and RenderBindings left from prior reconciles.
297
                if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
8✔
298
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderTasks after all bindings removed")
×
299
                }
×
300
                if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
8✔
301
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderBindings after all bindings removed")
×
302
                }
×
303

304
                return ctrl.Result{}, nil
8✔
305
        }
306

307
        // For each bound release, ensure a per-release RenderTask exists
308
        var releases []releaseInfo
100✔
309

100✔
310
        pendingDeps := false
100✔
311

100✔
312
        for _, binding := range bindingList.Items {
226✔
313
                rel := &solarv1alpha1.Release{}
126✔
314
                if err := r.Get(ctx, client.ObjectKey{
126✔
315
                        Name:      binding.Spec.ReleaseRef.Name,
126✔
316
                        Namespace: binding.Namespace,
126✔
317
                }, rel); err != nil {
126✔
318
                        if apierrors.IsNotFound(err) {
×
319
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
320
                                pendingDeps = true
×
321

×
322
                                continue
×
323
                        }
324

325
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
326
                }
327

328
                cv := &solarv1alpha1.ComponentVersion{}
126✔
329
                cvNamespace := rel.Namespace
126✔
330
                if rel.Spec.ComponentVersionNamespace != "" {
126✔
331
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
332
                }
×
333

334
                if cvNamespace != rel.Namespace {
126✔
335
                        granted := false
×
336
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
337
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
338
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
339
                        }
×
340
                        for i := range grantList.Items {
×
341
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
342
                                        granted = true
×
343
                                        break
×
344
                                }
345
                        }
346
                        if !granted {
×
347
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
348
                                pendingDeps = true
×
349

×
350
                                continue
×
351
                        }
352
                }
353

354
                if err := r.Get(ctx, client.ObjectKey{
126✔
355
                        Name:      rel.Spec.ComponentVersionRef.Name,
126✔
356
                        Namespace: cvNamespace,
126✔
357
                }, cv); err != nil {
126✔
358
                        if apierrors.IsNotFound(err) {
×
359
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
360
                                pendingDeps = true
×
361

×
362
                                continue
×
363
                        }
364

365
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
366
                }
367

368
                rtName := releaseRenderTaskName(rel.Namespace, rel.Name, target.Name, rel.GetGeneration())
126✔
369
                releases = append(releases, releaseInfo{
126✔
370
                        bindingKey: binding.Namespace + "/" + binding.Name,
126✔
371
                        name:       rel.Name,
126✔
372
                        release:    rel,
126✔
373
                        cv:         cv,
126✔
374
                        rtName:     rtName,
126✔
375
                })
126✔
376
        }
377

378
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
379
        var skipped []string
100✔
380
        releases, skipped = resolveReleaseConflicts(releases)
100✔
381
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
100✔
382
                return ctrl.Result{}, condErr
×
383
        }
×
384

385
        if len(releases) == 0 && !pendingDeps {
100✔
386
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
387
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
388
                        return ctrl.Result{}, condErr
×
389
                }
×
390

391
                return ctrl.Result{}, nil
×
392
        }
393

394
        // Create per-release RenderTasks (one per target+release pair).
395
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
396
        allRendered := true
100✔
397

100✔
398
        for i, ri := range releases {
217✔
399
                rt := &solarv1alpha1.RenderTask{}
117✔
400
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
117✔
401

117✔
402
                switch {
117✔
403
                case apierrors.IsNotFound(err):
29✔
404
                        spec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
29✔
405
                        if specErr != nil {
39✔
406
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
10✔
407
                                        specErr.Error()); condErr != nil {
10✔
408
                                        return ctrl.Result{}, condErr
×
409
                                }
×
410

411
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
10✔
412
                        }
413

414
                        rt = &solarv1alpha1.RenderTask{
19✔
415
                                ObjectMeta: metav1.ObjectMeta{
19✔
416
                                        Name:      ri.rtName,
19✔
417
                                        Namespace: target.Namespace,
19✔
418
                                },
19✔
419
                                Spec: spec,
19✔
420
                        }
19✔
421

19✔
422
                        if err := r.Create(ctx, rt); err != nil {
19✔
423
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
424
                        }
×
425

426
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
19✔
427
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
19✔
428
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
19✔
429
                case err != nil:
×
430
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
431
                default:
88✔
432
                        // RenderTask exists — check for spec drift (e.g. pull secrets
88✔
433
                        // changed after a RegistryBinding was created/updated).
88✔
434
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
88✔
435
                        if specErr != nil {
88✔
436
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
×
437
                                        specErr.Error()); condErr != nil {
×
438
                                        return ctrl.Result{}, condErr
×
439
                                }
×
440

441
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
442
                        }
443

444
                        if !apiequality.Semantic.DeepEqual(rt.Spec, desiredSpec) {
90✔
445
                                if err := r.Delete(ctx, rt); err != nil {
2✔
446
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete stale release RenderTask")
×
447
                                }
×
448

449
                                rt = &solarv1alpha1.RenderTask{
2✔
450
                                        ObjectMeta: metav1.ObjectMeta{
2✔
451
                                                Name:      ri.rtName,
2✔
452
                                                Namespace: target.Namespace,
2✔
453
                                        },
2✔
454
                                        Spec: desiredSpec,
2✔
455
                                }
2✔
456

2✔
457
                                if err := r.Create(ctx, rt); err != nil {
2✔
458
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to recreate release RenderTask")
×
459
                                }
×
460

461
                                log.V(1).Info("Recreated release RenderTask (spec drift)", "release", ri.name, "renderTask", ri.rtName)
2✔
462
                                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update",
2✔
463
                                        "Recreated release RenderTask %s for release %s (spec drift)", ri.rtName, ri.name)
2✔
464
                        }
465
                }
466

467
                // Check if release RenderTask is complete
468
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
107✔
469
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
470
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
471
                                return ctrl.Result{}, condErr
×
472
                        }
×
473

474
                        return ctrl.Result{}, nil
×
475
                }
476

477
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
151✔
478
                        releases[i].chartURL = rt.Status.ChartURL
44✔
479

44✔
480
                        // Ensure a RenderArtifact object exists for the pushed OCI artifact, and
44✔
481
                        // create a RenderBinding linking this Target to it.
44✔
482
                        aName := renderArtifactName(target.Namespace, rt.Spec.BaseURL, rt.Spec.Repository, rt.Spec.Tag)
44✔
483
                        bName := renderBindingName(aName, target.Name)
44✔
484
                        // Create the RenderBinding before the RenderArtifact to avoid a race
44✔
485
                        if err := r.ensureRenderBinding(ctx, target, aName, bName); err != nil {
44✔
486
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for release")
×
487
                        }
×
488
                        if err := r.ensureRenderArtifact(ctx, aName, rt, registry.Spec.Flavor, registryNamespace); err != nil {
44✔
489
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for release")
×
490
                        }
×
491
                        releases[i].artifactName = aName
44✔
492
                        releases[i].artifactBindingName = bName
44✔
493
                } else {
63✔
494
                        allRendered = false
63✔
495
                }
63✔
496
        }
497

498
        if pendingDeps {
90✔
499
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
500
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
501
                        return ctrl.Result{}, condErr
×
502
                }
×
503

504
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
505
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
×
506
        }
507

508
        if !allRendered {
150✔
509
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
60✔
510
                        "Waiting for release RenderTasks to complete"); condErr != nil {
61✔
511
                        return ctrl.Result{}, condErr
1✔
512
                }
1✔
513

514
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
59✔
515
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
59✔
516
        }
517

518
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
30✔
519
                "All releases rendered successfully"); condErr != nil {
30✔
520
                return ctrl.Result{}, condErr
×
521
        }
×
522

523
        // Determine if a new bootstrap render is needed by checking whether the
524
        // current bootstrapVersion's RenderTask still matches the desired release set.
525
        bootstrapVersion := target.Status.BootstrapVersion
30✔
526
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
30✔
527
        bootstrapRT := &solarv1alpha1.RenderTask{}
30✔
528
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
30✔
529

30✔
530
        needsNewBootstrap := false
30✔
531

30✔
532
        switch {
30✔
533
        case apierrors.IsNotFound(err):
5✔
534
                // No RenderTask for the current version yet — create one
5✔
535
                needsNewBootstrap = true
5✔
536
        case err != nil:
×
537
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
538
        default:
25✔
539
                // RenderTask exists — check if the desired bootstrap input changed
25✔
540
                // (release set, resolved refs/tags, or userdata)
25✔
541
                desiredInput, inputErr := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
25✔
542
                if inputErr != nil {
25✔
543
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
544
                }
×
545

546
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
25✔
547
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
28✔
548
                        bootstrapVersion++
3✔
549
                        needsNewBootstrap = true
3✔
550
                }
3✔
551
        }
552

553
        if needsNewBootstrap {
38✔
554
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
8✔
555
                if specErr != nil {
8✔
556
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
557
                }
×
558

559
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
8✔
560
                bootstrapRT = &solarv1alpha1.RenderTask{
8✔
561
                        ObjectMeta: metav1.ObjectMeta{
8✔
562
                                Name:      bootstrapRTName,
8✔
563
                                Namespace: target.Namespace,
8✔
564
                        },
8✔
565
                        Spec: spec,
8✔
566
                }
8✔
567

8✔
568
                if err := r.Create(ctx, bootstrapRT); err != nil {
9✔
569
                        if !apierrors.IsAlreadyExists(err) {
1✔
570
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
571
                        }
×
572

573
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
1✔
574
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
575
                        }
×
576
                } else {
7✔
577
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
7✔
578
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
579
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
7✔
580
                }
7✔
581

582
                // Persist the new bootstrapVersion in status
583
                if bootstrapVersion != target.Status.BootstrapVersion {
11✔
584
                        target.Status.BootstrapVersion = bootstrapVersion
3✔
585
                        if err := r.Status().Update(ctx, target); err != nil {
4✔
586
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
1✔
587
                        }
1✔
588
                }
589
        }
590

591
        // Update target status from bootstrap RenderTask
592
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
29✔
593
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
594
                        "Bootstrap rendering failed"); condErr != nil {
×
595
                        return ctrl.Result{}, condErr
×
596
                }
×
597

598
                return ctrl.Result{}, nil
×
599
        }
600

601
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
38✔
602
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
9✔
603
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
9✔
604
                        return ctrl.Result{}, condErr
×
605
                }
×
606

607
                // Ensure RenderArtifact + RenderBinding exist for the bootstrap chart.
608
                bootstrapArtifactName := renderArtifactName(target.Namespace, bootstrapRT.Spec.BaseURL, bootstrapRT.Spec.Repository, bootstrapRT.Spec.Tag)
9✔
609
                bootstrapBindingName := renderBindingName(bootstrapArtifactName, target.Name)
9✔
610
                // Create the RenderBinding before the RenderArtifact to avoid a race
9✔
611
                if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
9✔
612
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for bootstrap")
×
613
                }
×
614
                if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, registry.Spec.Flavor, registryNamespace); err != nil {
9✔
615
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for bootstrap")
×
616
                }
×
617

618
                // Clean up stale RenderTasks owned by this target (old versions)
619
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
9✔
620
                for _, ri := range releases {
23✔
621
                        currentRTNames[ri.rtName] = struct{}{}
14✔
622
                }
14✔
623
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
9✔
624
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
625
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
626
                        log.Error(err, "failed to clean up stale RenderTasks")
×
627
                }
×
628

629
                // Clean up stale RenderBindings owned by this target.
630
                currentBindingNames := map[string]struct{}{bootstrapBindingName: {}}
9✔
631
                for _, ri := range releases {
23✔
632
                        if ri.artifactBindingName != "" {
28✔
633
                                currentBindingNames[ri.artifactBindingName] = struct{}{}
14✔
634
                        }
14✔
635
                }
636
                if err := r.deleteStaleRenderBindings(ctx, target, currentBindingNames); err != nil {
9✔
637
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
638
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
639
                        log.Error(err, "failed to clean up stale RenderBindings")
×
640
                }
×
641

642
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
9✔
643
        }
644

645
        // Still running
646
        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
20✔
647
}
648

649
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
423✔
650
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
423✔
651
                Type:               condType,
423✔
652
                Status:             status,
423✔
653
                ObservedGeneration: target.Generation,
423✔
654
                Reason:             reason,
423✔
655
                Message:            message,
423✔
656
        })
423✔
657
        if changed {
522✔
658
                if err := r.Status().Update(ctx, target); err != nil {
101✔
659
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
2✔
660
                }
2✔
661
        }
662

663
        return nil
421✔
664
}
665

666
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
100✔
667
        if len(skipped) == 0 {
191✔
668
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
91✔
669
        }
91✔
670

671
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
672
}
673

674
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
675
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
676
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
677
// It returns the accepted releases and a slice of human-readable filter messages.
678
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
109✔
679
        if len(releases) == 0 {
110✔
680
                return releases, nil
1✔
681
        }
1✔
682

683
        // Step A: uniqueName deduplication.
684
        // When UniqueName is empty, fall back to the parent Component name from the CV.
685
        namedGroups := map[string][]releaseInfo{}
108✔
686

108✔
687
        for i, ri := range releases {
248✔
688
                uname := effectiveUniqueName(ri.release, ri.cv)
140✔
689
                releases[i].uniqueName = uname
140✔
690
                namedGroups[uname] = append(namedGroups[uname], releases[i])
140✔
691
        }
140✔
692

693
        var accepted []releaseInfo
108✔
694

108✔
695
        var skipped []string
108✔
696

108✔
697
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
108✔
698
        byPriority := func(a, b releaseInfo) bool {
140✔
699
                if a.release.Spec.Priority != b.release.Spec.Priority {
42✔
700
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
701
                }
10✔
702

703
                return a.bindingKey < b.bindingKey
22✔
704
        }
705

706
        uniqueNames := make([]string, 0, len(namedGroups))
108✔
707
        for k := range namedGroups {
239✔
708
                uniqueNames = append(uniqueNames, k)
131✔
709
        }
131✔
710

711
        sort.Strings(uniqueNames)
108✔
712

108✔
713
        for _, uniqueName := range uniqueNames {
239✔
714
                group := namedGroups[uniqueName]
131✔
715
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
140✔
716

717
                accepted = append(accepted, group[0])
131✔
718

131✔
719
                for _, loser := range group[1:] {
140✔
720
                        skipped = append(skipped, fmt.Sprintf(
9✔
721
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
722
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
723
                        ))
9✔
724
                }
9✔
725
        }
726

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

732
        resolved := make([]releaseInfo, 0, len(accepted))
108✔
733

108✔
734
        for _, ri := range accepted {
239✔
735
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
131✔
736
                var riSelector labels.Selector
131✔
737
                if ri.release.Spec.AntiAffinity != nil {
138✔
738
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
739
                        if err != nil {
8✔
740
                                skipped = append(skipped, fmt.Sprintf(
1✔
741
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
742
                                        ri.bindingKey, err,
1✔
743
                                ))
1✔
744

1✔
745
                                continue
1✔
746
                        }
747

748
                        riSelector = sel
6✔
749
                }
750

751
                // Check both directions: ri's anti-affinity against already-resolved labels,
752
                // and already-resolved anti-affinities against ri's labels.
753
                conflict := ""
130✔
754
                for _, other := range resolved {
153✔
755
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
27✔
756
                                conflict = other.bindingKey
4✔
757
                                break
4✔
758
                        }
759

760
                        if other.release.Spec.AntiAffinity != nil {
20✔
761
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
762
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
763
                                        conflict = other.bindingKey
1✔
764
                                        break
1✔
765
                                }
766
                        }
767
                }
768

769
                if conflict != "" {
135✔
770
                        skipped = append(skipped, fmt.Sprintf(
5✔
771
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
772
                                ri.bindingKey, conflict,
5✔
773
                        ))
5✔
774
                } else {
130✔
775
                        resolved = append(resolved, ri)
125✔
776
                }
125✔
777
        }
778

779
        return resolved, skipped
108✔
780
}
781

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

17✔
788
        rtList := &solarv1alpha1.RenderTaskList{}
17✔
789
        if err := r.List(ctx, rtList,
17✔
790
                client.InNamespace(target.Namespace),
17✔
791
                client.MatchingFields{indexOwnerKind: "Target"},
17✔
792
        ); err != nil {
17✔
793
                return err
×
794
        }
×
795

796
        for i := range rtList.Items {
43✔
797
                rt := &rtList.Items[i]
26✔
798
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
26✔
799
                        continue
×
800
                }
801

802
                if _, current := currentRTNames[rt.Name]; current {
49✔
803
                        continue
23✔
804
                }
805

806
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
3✔
807
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
3✔
808
                        return err
×
809
                }
×
810

811
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
3✔
812
                        "Deleted stale RenderTask %s", rt.Name)
3✔
813
        }
814

815
        return nil
17✔
816
}
817

818
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
819
        rtList := &solarv1alpha1.RenderTaskList{}
2✔
820
        if err := r.List(ctx, rtList,
2✔
821
                client.InNamespace(target.Namespace),
2✔
822
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
823
        ); err != nil {
2✔
824
                return err
×
825
        }
×
826

827
        for i := range rtList.Items {
4✔
828
                rt := &rtList.Items[i]
2✔
829
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
4✔
830
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
2✔
831
                                return err
×
832
                        }
×
833
                }
834
        }
835

836
        return nil
2✔
837
}
838

839
// deleteStaleRenderBindings removes RenderBindings owned by this target that are no
840
// longer needed (artifact is not in currentBindingNames).
841
func (r *TargetReconciler) deleteStaleRenderBindings(ctx context.Context, target *solarv1alpha1.Target, currentBindingNames map[string]struct{}) error {
17✔
842
        log := ctrl.LoggerFrom(ctx)
17✔
843

17✔
844
        bindingList := &solarv1alpha1.RenderBindingList{}
17✔
845
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
17✔
846
                return err
×
847
        }
×
848

849
        for i := range bindingList.Items {
43✔
850
                b := &bindingList.Items[i]
26✔
851
                if b.Spec.OwnerKind != "Target" || b.Spec.OwnerName != target.Name || b.Spec.OwnerNamespace != target.Namespace {
26✔
852
                        continue
×
853
                }
854

855
                if _, current := currentBindingNames[b.Name]; current {
49✔
856
                        continue
23✔
857
                }
858

859
                log.V(1).Info("Deleting stale RenderBinding", "renderBinding", b.Name)
3✔
860
                if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
3✔
861
                        return err
×
862
                }
×
863
        }
864

865
        return nil
17✔
866
}
867

868
// deleteOwnedRenderBindings removes all RenderBindings owned by this target.
869
// Called during Target deletion to trigger GC of any associated RenderArtifacts.
870
func (r *TargetReconciler) deleteOwnedRenderBindings(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
871
        bindingList := &solarv1alpha1.RenderBindingList{}
2✔
872
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
2✔
873
                return err
×
874
        }
×
875

876
        for i := range bindingList.Items {
3✔
877
                b := &bindingList.Items[i]
1✔
878
                if b.Spec.OwnerKind == "Target" && b.Spec.OwnerName == target.Name && b.Spec.OwnerNamespace == target.Namespace {
2✔
879
                        if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
1✔
880
                                return err
×
881
                        }
×
882
                }
883
        }
884

885
        return nil
2✔
886
}
887

888
// ensureRenderArtifact creates a RenderArtifact for the given RenderTask's OCI coordinates
889
// if one does not already exist. Idempotent: if it already exists (possibly created by
890
// another Target reconciling the same shared artifact), this is a no-op.
891
//
892
// pushSecretNamespace is passed explicitly because the secret may live in a different
893
// namespace than the RenderTask (e.g. a cluster-scoped secret namespace chosen by the
894
// operator). It must not be inferred from rt.Namespace.
895
func (r *TargetReconciler) ensureRenderArtifact(ctx context.Context, name string, rt *solarv1alpha1.RenderTask, flavor, pushSecretNamespace string) error {
53✔
896
        artifact := &solarv1alpha1.RenderArtifact{}
53✔
897
        if err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: rt.Namespace}, artifact); err == nil {
95✔
898
                if !artifact.DeletionTimestamp.IsZero() {
42✔
899
                        // The artifact is terminating (OCI cleanup in progress). Creating a binding
×
900
                        // against it would race with the finalizer. Requeue and wait for full deletion.
×
901
                        return fmt.Errorf("RenderArtifact %s/%s is terminating; requeuing", rt.Namespace, name)
×
902
                }
×
903

904
                return nil
42✔
905
        } else if !apierrors.IsNotFound(err) {
11✔
906
                return err
×
907
        }
×
908

909
        artifact = &solarv1alpha1.RenderArtifact{
11✔
910
                ObjectMeta: metav1.ObjectMeta{
11✔
911
                        Name:      name,
11✔
912
                        Namespace: rt.Namespace,
11✔
913
                },
11✔
914
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
915
                        BaseURL:             rt.Spec.BaseURL,
11✔
916
                        Repository:          rt.Spec.Repository,
11✔
917
                        Tag:                 rt.Spec.Tag,
11✔
918
                        RenderTaskRef:       rt.Name,
11✔
919
                        PushSecretRef:       rt.Spec.PushSecretRef,
11✔
920
                        PushSecretNamespace: pushSecretNamespace,
11✔
921
                        RegistryFlavor:      flavor,
11✔
922
                        PlainHTTP:           rt.Spec.PlainHTTP,
11✔
923
                },
11✔
924
        }
11✔
925

11✔
926
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
927
                return err
×
928
        }
×
929

930
        return nil
11✔
931
}
932

933
// ensureRenderBinding creates a RenderBinding linking this Target to the named
934
// RenderArtifact if one does not already exist. Idempotent.
935
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string) error {
53✔
936
        binding := &solarv1alpha1.RenderBinding{}
53✔
937
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
95✔
938
                return nil
42✔
939
        } else if !apierrors.IsNotFound(err) {
53✔
940
                return err
×
941
        }
×
942

943
        binding = &solarv1alpha1.RenderBinding{
11✔
944
                ObjectMeta: metav1.ObjectMeta{
11✔
945
                        Name:      bindingName,
11✔
946
                        Namespace: target.Namespace,
11✔
947
                },
11✔
948
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
949
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
950
                        OwnerKind:         "Target",
11✔
951
                        OwnerName:         target.Name,
11✔
952
                        OwnerNamespace:    target.Namespace,
11✔
953
                },
11✔
954
        }
11✔
955

11✔
956
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
957
                return err
×
958
        }
×
959

960
        return nil
11✔
961
}
962

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

117✔
967
        var targetNamespace string
117✔
968
        if rel.Spec.TargetNamespace != nil {
234✔
969
                targetNamespace = *rel.Spec.TargetNamespace
117✔
970
        }
117✔
971

972
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
117✔
973
        if err != nil {
127✔
974
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
975
        }
10✔
976

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

107✔
983
        return solarv1alpha1.RenderTaskSpec{
107✔
984
                RendererConfig: solarv1alpha1.RendererConfig{
107✔
985
                        Type: solarv1alpha1.RendererConfigTypeRelease,
107✔
986
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
107✔
987
                                Chart: solarv1alpha1.ChartConfig{
107✔
988
                                        Name:        chartName,
107✔
989
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
107✔
990
                                        Version:     tag,
107✔
991
                                        AppVersion:  tag,
107✔
992
                                },
107✔
993
                                Input: solarv1alpha1.ReleaseInput{
107✔
994
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
107✔
995
                                        Resources:  resolvedResources,
107✔
996
                                        Entrypoint: cv.Spec.Entrypoint,
107✔
997
                                },
107✔
998
                                Values:          rel.Spec.Values,
107✔
999
                                TargetNamespace: targetNamespace,
107✔
1000
                        },
107✔
1001
                },
107✔
1002
                Repository:     repo,
107✔
1003
                Tag:            tag,
107✔
1004
                BaseURL:        registry.Spec.Hostname,
107✔
1005
                PlainHTTP:      registry.Spec.PlainHTTP,
107✔
1006
                PushSecretRef:  registry.Spec.SolarSecretRef,
107✔
1007
                FailedJobTTL:   rel.Spec.FailedJobTTL,
107✔
1008
                OwnerName:      target.Name,
107✔
1009
                OwnerNamespace: target.Namespace,
107✔
1010
                OwnerKind:      "Target",
107✔
1011
        }, nil
107✔
1012
}
1013

1014
// buildBootstrapInput constructs the desired BootstrapInput from the current
1015
// target and resolved releases. Used for both comparison and spec construction.
1016
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string) (solarv1alpha1.BootstrapInput, error) {
35✔
1017
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
35✔
1018

35✔
1019
        for _, ri := range releases {
84✔
1020
                if ri.uniqueName == "" {
50✔
1021
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
1022
                }
1✔
1023

1024
                ref, err := ociname.ParseReference(ri.chartURL)
48✔
1025
                if err != nil {
48✔
1026
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
1027
                }
×
1028

1029
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
48✔
1030
                if err != nil {
48✔
1031
                        return solarv1alpha1.BootstrapInput{}, err
×
1032
                }
×
1033

1034
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
48✔
1035
                        Repository:     strings.TrimPrefix(repo, "oci://"),
48✔
1036
                        Tag:            ref.Identifier(),
48✔
1037
                        PullSecretName: renderRegistryPullSecret,
48✔
1038
                }
48✔
1039
        }
1040

1041
        return solarv1alpha1.BootstrapInput{
34✔
1042
                Releases: resolvedReleases,
34✔
1043
                Userdata: target.Spec.Userdata,
34✔
1044
        }, nil
34✔
1045
}
1046

1047
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
8✔
1048
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
8✔
1049
        if err != nil {
8✔
1050
                return solarv1alpha1.RenderTaskSpec{}, err
×
1051
        }
×
1052

1053
        releaseNames := make([]string, 0, len(releases))
8✔
1054
        for _, ri := range releases {
19✔
1055
                releaseNames = append(releaseNames, ri.name)
11✔
1056
        }
11✔
1057

1058
        sort.Strings(releaseNames)
8✔
1059

8✔
1060
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
8✔
1061
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
8✔
1062
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
8✔
1063

8✔
1064
        return solarv1alpha1.RenderTaskSpec{
8✔
1065
                RendererConfig: solarv1alpha1.RendererConfig{
8✔
1066
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
8✔
1067
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
8✔
1068
                                Chart: solarv1alpha1.ChartConfig{
8✔
1069
                                        Name:        chartName,
8✔
1070
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
8✔
1071
                                        Version:     tag,
8✔
1072
                                        AppVersion:  tag,
8✔
1073
                                },
8✔
1074
                                Input: input,
8✔
1075
                        },
8✔
1076
                },
8✔
1077
                Repository:     repo,
8✔
1078
                Tag:            tag,
8✔
1079
                BaseURL:        registry.Spec.Hostname,
8✔
1080
                PlainHTTP:      registry.Spec.PlainHTTP,
8✔
1081
                PushSecretRef:  registry.Spec.SolarSecretRef,
8✔
1082
                OwnerName:      target.Name,
8✔
1083
                OwnerNamespace: target.Namespace,
8✔
1084
                OwnerKind:      "Target",
8✔
1085
        }, nil
8✔
1086
}
1087

1088
// SetupWithManager sets up the controller with the Manager.
1089
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
1090
        return ctrl.NewControllerManagedBy(mgr).
1✔
1091
                For(&solarv1alpha1.Target{}).
1✔
1092
                Watches(
1✔
1093
                        &solarv1alpha1.ReleaseBinding{},
1✔
1094
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
1095
                ).
1✔
1096
                Watches(
1✔
1097
                        &solarv1alpha1.RenderTask{},
1✔
1098
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
1099
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
1100
                ).
1✔
1101
                Watches(
1✔
1102
                        &solarv1alpha1.Registry{},
1✔
1103
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
1104
                ).
1✔
1105
                Watches(
1✔
1106
                        &solarv1alpha1.RegistryBinding{},
1✔
1107
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
1108
                ).
1✔
1109
                Watches(
1✔
1110
                        &solarv1alpha1.ReferenceGrant{},
1✔
1111
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
1112
                ).
1✔
1113
                Watches(
1✔
1114
                        &solarv1alpha1.Release{},
1✔
1115
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
1116
                ).
1✔
1117
                Complete(r)
1✔
1118
}
1✔
1119

1120
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
1121
// fromNamespace to reference the named registry.
1122
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
1123
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
1124
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
1125
                return false, err
×
1126
        }
×
1127
        for i := range grantList.Items {
×
1128
                grant := &grantList.Items[i]
×
1129
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
1130
                        return true, nil
×
1131
                }
×
1132
        }
1133

1134
        return false, nil
×
1135
}
1136

1137
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
1138
// fromNamespace to reference Registry resources in the grant's namespace.
1139
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
1140
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
1141
}
×
1142

1143
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1144
// Targets that reference it — either in the same namespace or cross-namespace.
1145
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
205✔
1146
        reg, ok := obj.(*solarv1alpha1.Registry)
205✔
1147
        if !ok {
205✔
1148
                return nil
×
1149
        }
×
1150

1151
        // Same-namespace targets
1152
        targetList := &solarv1alpha1.TargetList{}
205✔
1153
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
205✔
1154
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
1155

×
1156
                return nil
×
1157
        }
×
1158

1159
        var requests []reconcile.Request
205✔
1160
        for _, t := range targetList.Items {
272✔
1161
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
67✔
1162
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
121✔
1163
                        requests = append(requests, reconcile.Request{
54✔
1164
                                NamespacedName: types.NamespacedName{
54✔
1165
                                        Name:      t.Name,
54✔
1166
                                        Namespace: t.Namespace,
54✔
1167
                                },
54✔
1168
                        })
54✔
1169
                }
54✔
1170
        }
1171

1172
        // Cross-namespace targets: find namespaces that have been granted access to
1173
        // registries in reg.Namespace, then check their targets.
1174
        grantList := &solarv1alpha1.ReferenceGrantList{}
205✔
1175
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
205✔
1176
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
1177
                return requests
×
1178
        }
×
1179

1180
        for i := range grantList.Items {
223✔
1181
                grant := &grantList.Items[i]
18✔
1182
                if !grantsRegistryResource(grant) {
36✔
1183
                        continue
18✔
1184
                }
1185
                for _, from := range grant.Spec.From {
×
1186
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1187
                                continue
×
1188
                        }
1189
                        crossTargets := &solarv1alpha1.TargetList{}
×
1190
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
1191
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
1192
                                continue
×
1193
                        }
1194
                        for _, t := range crossTargets.Items {
×
1195
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
1196
                                        requests = append(requests, reconcile.Request{
×
1197
                                                NamespacedName: types.NamespacedName{
×
1198
                                                        Name:      t.Name,
×
1199
                                                        Namespace: t.Namespace,
×
1200
                                                },
×
1201
                                        })
×
1202
                                }
×
1203
                        }
1204
                }
1205
        }
1206

1207
        return requests
205✔
1208
}
1209

1210
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
1211
// each bound Registry, and returns a map from registry hostname to
1212
// targetPullSecretName. Registries without a targetPullSecretName are included
1213
// with an empty string (anonymous pull).
1214
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
128✔
1215
        rbList := &solarv1alpha1.RegistryBindingList{}
128✔
1216
        if err := r.List(ctx, rbList,
128✔
1217
                client.InNamespace(target.Namespace),
128✔
1218
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
128✔
1219
        ); err != nil {
128✔
1220
                return nil, err
×
1221
        }
×
1222

1223
        type hostEntry struct {
128✔
1224
                pullSecret  string
128✔
1225
                bindingName string
128✔
1226
        }
128✔
1227

128✔
1228
        lookup := make(map[string]hostEntry, len(rbList.Items))
128✔
1229

128✔
1230
        for _, rb := range rbList.Items {
168✔
1231
                reg := &solarv1alpha1.Registry{}
40✔
1232
                if err := r.Get(ctx, client.ObjectKey{
40✔
1233
                        Name:      rb.Spec.RegistryRef.Name,
40✔
1234
                        Namespace: rb.Namespace,
40✔
1235
                }, reg); err != nil {
50✔
1236
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1237
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1238
                }
10✔
1239

1240
                host := strings.ToLower(reg.Spec.Hostname)
30✔
1241
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
40✔
1242
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
10✔
1243
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1244
                }
10✔
1245

1246
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
20✔
1247
        }
1248

1249
        result := make(map[string]string, len(lookup))
108✔
1250
        for host, entry := range lookup {
118✔
1251
                result[host] = entry.pullSecret
10✔
1252
        }
10✔
1253

1254
        return result, nil
108✔
1255
}
1256

1257
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1258
// for the referenced Target.
1259
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
66✔
1260
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
66✔
1261
        if !ok {
66✔
1262
                return nil
×
1263
        }
×
1264

1265
        if rb.Spec.TargetRef.Name == "" {
66✔
1266
                return nil
×
1267
        }
×
1268

1269
        return []reconcile.Request{
66✔
1270
                {
66✔
1271
                        NamespacedName: types.NamespacedName{
66✔
1272
                                Name:      rb.Spec.TargetRef.Name,
66✔
1273
                                Namespace: rb.Namespace,
66✔
1274
                        },
66✔
1275
                },
66✔
1276
        }
66✔
1277
}
1278

1279
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1280
// either because the grant controls Registry access (Target → Registry) or because
1281
// it controls ComponentVersion access (Release → ComponentVersion).
1282
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
14✔
1283
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
14✔
1284
        if !ok {
14✔
1285
                return nil
×
1286
        }
×
1287

1288
        var requests []reconcile.Request
14✔
1289

14✔
1290
        if grantsRegistryResource(grant) {
14✔
1291
                for _, from := range grant.Spec.From {
×
1292
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1293
                                continue
×
1294
                        }
1295
                        targets := &solarv1alpha1.TargetList{}
×
1296
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
1297
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1298
                                continue
×
1299
                        }
1300
                        for _, t := range targets.Items {
×
1301
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
1302
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
1303
                                        requests = append(requests, reconcile.Request{
×
1304
                                                NamespacedName: types.NamespacedName{
×
1305
                                                        Name:      t.Name,
×
1306
                                                        Namespace: t.Namespace,
×
1307
                                                },
×
1308
                                        })
×
1309
                                }
×
1310
                        }
1311
                }
1312
        }
1313

1314
        if grantsComponentVersionResource(grant) {
20✔
1315
                seen := map[string]struct{}{}
6✔
1316
                for _, from := range grant.Spec.From {
12✔
1317
                        if from.Kind != "Release" || from.Group != solarGroup {
6✔
1318
                                continue
×
1319
                        }
1320
                        bindings := &solarv1alpha1.ReleaseBindingList{}
6✔
1321
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
6✔
1322
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1323
                                continue
×
1324
                        }
1325
                        for _, rb := range bindings.Items {
7✔
1326
                                if rb.Spec.TargetRef.Name == "" {
1✔
1327
                                        continue
×
1328
                                }
1329
                                targetNs := rb.Namespace
1✔
1330
                                if rb.Spec.TargetNamespace != "" {
2✔
1331
                                        targetNs = rb.Spec.TargetNamespace
1✔
1332
                                }
1✔
1333
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1334
                                if _, ok := seen[key]; ok {
1✔
1335
                                        continue
×
1336
                                }
1337
                                seen[key] = struct{}{}
1✔
1338
                                requests = append(requests, reconcile.Request{
1✔
1339
                                        NamespacedName: types.NamespacedName{
1✔
1340
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1341
                                                Namespace: targetNs,
1✔
1342
                                        },
1✔
1343
                                })
1✔
1344
                        }
1345
                }
1346
        }
1347

1348
        if grantsReleaseBindingToTargetResource(grant) {
18✔
1349
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1350
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1351
                // the new or removed cross-namespace ReleaseBindings.
4✔
1352
                targets := &solarv1alpha1.TargetList{}
4✔
1353
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1354
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1355
                } else {
4✔
1356
                        for _, t := range targets.Items {
8✔
1357
                                requests = append(requests, reconcile.Request{
4✔
1358
                                        NamespacedName: types.NamespacedName{
4✔
1359
                                                Name:      t.Name,
4✔
1360
                                                Namespace: t.Namespace,
4✔
1361
                                        },
4✔
1362
                                })
4✔
1363
                        }
4✔
1364
                }
1365
        }
1366

1367
        return requests
14✔
1368
}
1369

1370
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1371
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
32✔
1372
        for _, t := range grant.Spec.To {
64✔
1373
                if t.Kind == "Registry" && t.Group == solarGroup {
32✔
1374
                        return true
×
1375
                }
×
1376
        }
1377

1378
        return false
32✔
1379
}
1380

1381
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1382
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1383
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
26✔
1384
        hasReleaseBindingFrom := false
26✔
1385
        for _, f := range grant.Spec.From {
52✔
1386
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
42✔
1387
                        hasReleaseBindingFrom = true
16✔
1388
                        break
16✔
1389
                }
1390
        }
1391
        if !hasReleaseBindingFrom {
36✔
1392
                return false
10✔
1393
        }
10✔
1394
        for _, t := range grant.Spec.To {
32✔
1395
                if t.Kind == "Target" && t.Group == solarGroup {
32✔
1396
                        return true
16✔
1397
                }
16✔
1398
        }
1399

1400
        return false
×
1401
}
1402

1403
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1404
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1405
// a ReferenceGrant in target's namespace.
1406
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
108✔
1407
        grantList := &solarv1alpha1.ReferenceGrantList{}
108✔
1408
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
108✔
1409
                return nil, err
×
1410
        }
×
1411

1412
        seen := make(map[string]struct{})
108✔
1413
        var result []solarv1alpha1.ReleaseBinding
108✔
1414
        for i := range grantList.Items {
120✔
1415
                grant := &grantList.Items[i]
12✔
1416
                if !grantsReleaseBindingToTargetResource(grant) {
12✔
1417
                        continue
×
1418
                }
1419
                for _, from := range grant.Spec.From {
24✔
1420
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
12✔
1421
                                continue
×
1422
                        }
1423
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
12✔
1424
                        if err := r.List(ctx, crossBindings,
12✔
1425
                                client.InNamespace(from.Namespace),
12✔
1426
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
12✔
1427
                        ); err != nil {
12✔
1428
                                return nil, err
×
1429
                        }
×
1430
                        for _, rb := range crossBindings.Items {
24✔
1431
                                if rb.Spec.TargetNamespace != target.Namespace {
12✔
1432
                                        continue
×
1433
                                }
1434
                                key := rb.Namespace + "/" + rb.Name
12✔
1435
                                if _, exists := seen[key]; exists {
15✔
1436
                                        continue
3✔
1437
                                }
1438
                                seen[key] = struct{}{}
9✔
1439
                                result = append(result, rb)
9✔
1440
                        }
1441
                }
1442
        }
1443

1444
        return result, nil
108✔
1445
}
1446

1447
// mapReleaseToTargets maps a Release event to reconcile requests for all
1448
// Targets that are bound to the release via ReleaseBindings.
1449
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
428✔
1450
        rel, ok := obj.(*solarv1alpha1.Release)
428✔
1451
        if !ok {
428✔
1452
                return nil
×
1453
        }
×
1454

1455
        bindingList := &solarv1alpha1.ReleaseBindingList{}
428✔
1456
        if err := r.List(ctx, bindingList,
428✔
1457
                client.InNamespace(rel.Namespace),
428✔
1458
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
428✔
1459
        ); err != nil {
428✔
1460
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1461

×
1462
                return nil
×
1463
        }
×
1464

1465
        seen := map[string]struct{}{}
428✔
1466
        var requests []reconcile.Request
428✔
1467

428✔
1468
        for _, rb := range bindingList.Items {
628✔
1469
                targetNs := rb.Namespace
200✔
1470
                if rb.Spec.TargetNamespace != "" {
200✔
1471
                        targetNs = rb.Spec.TargetNamespace
×
1472
                }
×
1473

1474
                key := targetNs + "/" + rb.Spec.TargetRef.Name
200✔
1475
                if _, ok := seen[key]; ok {
204✔
1476
                        continue
4✔
1477
                }
1478

1479
                seen[key] = struct{}{}
196✔
1480
                requests = append(requests, reconcile.Request{
196✔
1481
                        NamespacedName: types.NamespacedName{
196✔
1482
                                Name:      rb.Spec.TargetRef.Name,
196✔
1483
                                Namespace: targetNs,
196✔
1484
                        },
196✔
1485
                })
196✔
1486
        }
1487

1488
        return requests
428✔
1489
}
1490

1491
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
235✔
1492
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
235✔
1493
        if !ok || rb.Spec.TargetRef.Name == "" {
235✔
1494
                return nil
×
1495
        }
×
1496

1497
        targetNs := rb.Namespace
235✔
1498
        if rb.Spec.TargetNamespace != "" {
252✔
1499
                targetNs = rb.Spec.TargetNamespace
17✔
1500
        }
17✔
1501

1502
        return []reconcile.Request{
235✔
1503
                {
235✔
1504
                        NamespacedName: types.NamespacedName{
235✔
1505
                                Name:      rb.Spec.TargetRef.Name,
235✔
1506
                                Namespace: targetNs,
235✔
1507
                        },
235✔
1508
                },
235✔
1509
        }
235✔
1510
}
1511

1512
// removeRegistryRefFinalizer removes registryRefFinalizer from registry when no other active
1513
// Target or RegistryBinding (excluding the deleting Target) still references it.
1514
func (r *TargetReconciler) removeRegistryRefFinalizer(ctx context.Context, deletingTarget *solarv1alpha1.Target, registry *solarv1alpha1.Registry) error {
2✔
1515
        return removeRegistryRefFinalizer(ctx, r.Client, deletingTarget, nil, registry)
2✔
1516
}
2✔
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