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

opendefensecloud / solution-arsenal / 27409535067

12 Jun 2026 10:16AM UTC coverage: 74.408% (+0.4%) from 74.029%
27409535067

Pull #584

github

web-flow
Merge 759c774a0 into 762f047da
Pull Request #584: feat: improve resource polling and backoff

28 of 30 new or added lines in 2 files covered. (93.33%)

296 existing lines in 5 files now uncovered.

2986 of 4013 relevant lines covered (74.41%)

37.06 hits per line

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

72.59
/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
        // WatchNamespace restricts reconciliation to this namespace.
69
        // Should be empty in production (watches all namespaces).
70
        // Intended for use in integration tests only.
71
        WatchNamespace string
72
        // RegistryBindingStrict enables strict registry binding mode.
73
        // When true, rendering fails if a resource's registry host has no
74
        // matching RegistryBinding. When false (default/relaxed), unmatched
75
        // hosts are treated as anonymous pull (no secretRef rendered).
76
        RegistryBindingStrict bool
77
}
78

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

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

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

291✔
100
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
382✔
101
                return ctrl.Result{}, nil
91✔
102
        }
91✔
103

104
        // Fetch target
105
        target := &solarv1alpha1.Target{}
200✔
106
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
205✔
107
                if apierrors.IsNotFound(err) {
10✔
108
                        return ctrl.Result{}, nil
5✔
109
                }
5✔
110

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

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

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

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

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

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

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

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

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

161
                return ctrl.Result{}, nil
37✔
162
        }
163

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

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

183
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
UNCOV
184
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
×
185
                }
186
        }
187

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

199
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
22✔
200
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
22✔
201
                }
202

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

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

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

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

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

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

231
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
232
        // Filter on targetNamespace="" to exclude cross-namespace bindings (targetNamespace set) that share the
233
        // target name but point to a target in a different namespace.
234
        bindingList := &solarv1alpha1.ReleaseBindingList{}
112✔
235
        if err := r.List(ctx, bindingList,
112✔
236
                client.InNamespace(target.Namespace),
112✔
237
                client.MatchingFields{
112✔
238
                        indexReleaseBindingTargetName:      target.Name,
112✔
239
                        indexReleaseBindingTargetNamespace: "",
112✔
240
                },
112✔
241
        ); err != nil {
112✔
UNCOV
242
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
UNCOV
243
        }
×
244

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

112✔
252
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
112✔
253
        // RegistryBinding collection is wired into the rendering pipeline.
112✔
254

112✔
255
        if len(bindingList.Items) == 0 {
123✔
256
                log.V(1).Info("No ReleaseBindings found for target")
11✔
257
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
11✔
258
                        "No ReleaseBindings found for this target"); condErr != nil {
11✔
UNCOV
259
                        return ctrl.Result{}, condErr
×
UNCOV
260
                }
×
261

262
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
11✔
263
                        "No ReleaseBindings found for this target"); condErr != nil {
12✔
264
                        return ctrl.Result{}, condErr
1✔
265
                }
1✔
266

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

275
                return ctrl.Result{}, nil
10✔
276
        }
277

278
        // For each bound release, ensure a per-release RenderTask exists
279
        var releases []releaseInfo
101✔
280

101✔
281
        pendingDeps := false
101✔
282

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

×
UNCOV
293
                                continue
×
294
                        }
295

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

299
                cv := &solarv1alpha1.ComponentVersion{}
129✔
300
                cvNamespace := rel.Namespace
129✔
301
                if rel.Spec.ComponentVersionNamespace != "" {
129✔
UNCOV
302
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
UNCOV
303
                }
×
304

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

×
UNCOV
321
                                continue
×
322
                        }
323
                }
324

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

×
UNCOV
333
                                continue
×
334
                        }
335

336
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
337
                }
338

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

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

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

UNCOV
362
                return ctrl.Result{}, nil
×
363
        }
364

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

101✔
369
        for i, ri := range releases {
220✔
370
                rt := &solarv1alpha1.RenderTask{}
119✔
371
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
119✔
372

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

382
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
10✔
383
                        }
384

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

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

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

UNCOV
412
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
413
                        }
414

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

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

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

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

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

UNCOV
445
                        return ctrl.Result{}, nil
×
446
                }
447

448
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
155✔
449
                        releases[i].chartURL = rt.Status.ChartURL
46✔
450

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

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

UNCOV
475
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
UNCOV
476
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
×
477
        }
478

479
        if !allRendered {
151✔
480
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
60✔
481
                        "Waiting for release RenderTasks to complete"); condErr != nil {
61✔
482
                        return ctrl.Result{}, condErr
1✔
483
                }
1✔
484

485
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
59✔
486
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
59✔
487
        }
488

489
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
31✔
490
                "All releases rendered successfully"); condErr != nil {
31✔
491
                return ctrl.Result{}, condErr
×
492
        }
×
493

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

31✔
501
        needsNewBootstrap := false
31✔
502

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

517
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
26✔
518
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
28✔
519
                        bootstrapVersion++
2✔
520
                        needsNewBootstrap = true
2✔
521
                }
2✔
522
        }
523

524
        if needsNewBootstrap {
38✔
525
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
7✔
526
                if specErr != nil {
7✔
UNCOV
527
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
528
                }
×
529

530
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
7✔
531
                bootstrapRT = &solarv1alpha1.RenderTask{
7✔
532
                        ObjectMeta: metav1.ObjectMeta{
7✔
533
                                Name:      bootstrapRTName,
7✔
534
                                Namespace: target.Namespace,
7✔
535
                        },
7✔
536
                        Spec: spec,
7✔
537
                }
7✔
538

7✔
539
                if err := r.Create(ctx, bootstrapRT); err != nil {
7✔
540
                        if !apierrors.IsAlreadyExists(err) {
×
UNCOV
541
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
UNCOV
542
                        }
×
543

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

553
                // Persist the new bootstrapVersion in status
554
                if bootstrapVersion != target.Status.BootstrapVersion {
9✔
555
                        target.Status.BootstrapVersion = bootstrapVersion
2✔
556
                        if err := r.Status().Update(ctx, target); err != nil {
2✔
UNCOV
557
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
UNCOV
558
                        }
×
559
                }
560
        }
561

562
        // Update target status from bootstrap RenderTask
563
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
31✔
UNCOV
564
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
UNCOV
565
                        "Bootstrap rendering failed"); condErr != nil {
×
UNCOV
566
                        return ctrl.Result{}, condErr
×
UNCOV
567
                }
×
568

UNCOV
569
                return ctrl.Result{}, nil
×
570
        }
571

572
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
39✔
573
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
8✔
574
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
8✔
UNCOV
575
                        return ctrl.Result{}, condErr
×
UNCOV
576
                }
×
577

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

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

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

613
                return ctrl.Result{}, nil
8✔
614
        }
615

616
        // Still running
617
        return ctrl.Result{}, nil
23✔
618
}
619

620
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
408✔
621
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
408✔
622
                Type:               condType,
408✔
623
                Status:             status,
408✔
624
                ObservedGeneration: target.Generation,
408✔
625
                Reason:             reason,
408✔
626
                Message:            message,
408✔
627
        })
408✔
628
        if changed {
506✔
629
                if err := r.Status().Update(ctx, target); err != nil {
100✔
630
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
2✔
631
                }
2✔
632
        }
633

634
        return nil
406✔
635
}
636

637
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
101✔
638
        if len(skipped) == 0 {
192✔
639
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
91✔
640
        }
91✔
641

642
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
10✔
643
}
644

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

654
        // Step A: uniqueName deduplication.
655
        // When UniqueName is empty, fall back to the parent Component name from the CV.
656
        namedGroups := map[string][]releaseInfo{}
109✔
657

109✔
658
        for i, ri := range releases {
252✔
659
                uname := effectiveUniqueName(ri.release, ri.cv)
143✔
660
                releases[i].uniqueName = uname
143✔
661
                namedGroups[uname] = append(namedGroups[uname], releases[i])
143✔
662
        }
143✔
663

664
        var accepted []releaseInfo
109✔
665

109✔
666
        var skipped []string
109✔
667

109✔
668
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
109✔
669
        byPriority := func(a, b releaseInfo) bool {
143✔
670
                if a.release.Spec.Priority != b.release.Spec.Priority {
45✔
671
                        return a.release.Spec.Priority > b.release.Spec.Priority
11✔
672
                }
11✔
673

674
                return a.bindingKey < b.bindingKey
23✔
675
        }
676

677
        uniqueNames := make([]string, 0, len(namedGroups))
109✔
678
        for k := range namedGroups {
242✔
679
                uniqueNames = append(uniqueNames, k)
133✔
680
        }
133✔
681

682
        sort.Strings(uniqueNames)
109✔
683

109✔
684
        for _, uniqueName := range uniqueNames {
242✔
685
                group := namedGroups[uniqueName]
133✔
686
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
143✔
687

688
                accepted = append(accepted, group[0])
133✔
689

133✔
690
                for _, loser := range group[1:] {
143✔
691
                        skipped = append(skipped, fmt.Sprintf(
10✔
692
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
10✔
693
                                loser.bindingKey, uniqueName, group[0].bindingKey,
10✔
694
                        ))
10✔
695
                }
10✔
696
        }
697

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

703
        resolved := make([]releaseInfo, 0, len(accepted))
109✔
704

109✔
705
        for _, ri := range accepted {
242✔
706
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
133✔
707
                var riSelector labels.Selector
133✔
708
                if ri.release.Spec.AntiAffinity != nil {
140✔
709
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
710
                        if err != nil {
8✔
711
                                skipped = append(skipped, fmt.Sprintf(
1✔
712
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
713
                                        ri.bindingKey, err,
1✔
714
                                ))
1✔
715

1✔
716
                                continue
1✔
717
                        }
718

719
                        riSelector = sel
6✔
720
                }
721

722
                // Check both directions: ri's anti-affinity against already-resolved labels,
723
                // and already-resolved anti-affinities against ri's labels.
724
                conflict := ""
132✔
725
                for _, other := range resolved {
156✔
726
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
28✔
727
                                conflict = other.bindingKey
4✔
728
                                break
4✔
729
                        }
730

731
                        if other.release.Spec.AntiAffinity != nil {
21✔
732
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
733
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
734
                                        conflict = other.bindingKey
1✔
735
                                        break
1✔
736
                                }
737
                        }
738
                }
739

740
                if conflict != "" {
137✔
741
                        skipped = append(skipped, fmt.Sprintf(
5✔
742
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
743
                                ri.bindingKey, conflict,
5✔
744
                        ))
5✔
745
                } else {
132✔
746
                        resolved = append(resolved, ri)
127✔
747
                }
127✔
748
        }
749

750
        return resolved, skipped
109✔
751
}
752

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

18✔
759
        rtList := &solarv1alpha1.RenderTaskList{}
18✔
760
        if err := r.List(ctx, rtList,
18✔
761
                client.InNamespace(target.Namespace),
18✔
762
                client.MatchingFields{indexOwnerKind: "Target"},
18✔
763
        ); err != nil {
18✔
UNCOV
764
                return err
×
UNCOV
765
        }
×
766

767
        for i := range rtList.Items {
41✔
768
                rt := &rtList.Items[i]
23✔
769
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
23✔
UNCOV
770
                        continue
×
771
                }
772

773
                if _, current := currentRTNames[rt.Name]; current {
43✔
774
                        continue
20✔
775
                }
776

777
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
3✔
778
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
3✔
UNCOV
779
                        return err
×
UNCOV
780
                }
×
781

782
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
3✔
783
                        "Deleted stale RenderTask %s", rt.Name)
3✔
784
        }
785

786
        return nil
18✔
787
}
788

789
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
790
        rtList := &solarv1alpha1.RenderTaskList{}
2✔
791
        if err := r.List(ctx, rtList,
2✔
792
                client.InNamespace(target.Namespace),
2✔
793
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
794
        ); err != nil {
2✔
UNCOV
795
                return err
×
UNCOV
796
        }
×
797

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

807
        return nil
2✔
808
}
809

810
// deleteStaleRenderBindings removes RenderBindings owned by this target that are no
811
// longer needed (artifact is not in currentBindingNames).
812
func (r *TargetReconciler) deleteStaleRenderBindings(ctx context.Context, target *solarv1alpha1.Target, currentBindingNames map[string]struct{}) error {
18✔
813
        log := ctrl.LoggerFrom(ctx)
18✔
814

18✔
815
        bindingList := &solarv1alpha1.RenderBindingList{}
18✔
816
        if err := r.List(ctx, bindingList,
18✔
817
                client.InNamespace(target.Namespace),
18✔
818
                client.MatchingFields{indexOwnerKind: "Target"},
18✔
819
        ); err != nil {
18✔
UNCOV
820
                return err
×
UNCOV
821
        }
×
822

823
        for i := range bindingList.Items {
42✔
824
                b := &bindingList.Items[i]
24✔
825
                if b.Spec.OwnerName != target.Name || b.Spec.OwnerNamespace != target.Namespace {
24✔
UNCOV
826
                        continue
×
827
                }
828

829
                if _, current := currentBindingNames[b.Name]; current {
44✔
830
                        continue
20✔
831
                }
832

833
                log.V(1).Info("Deleting stale RenderBinding", "renderBinding", b.Name)
4✔
834
                if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
4✔
UNCOV
835
                        return err
×
UNCOV
836
                }
×
837
        }
838

839
        return nil
18✔
840
}
841

842
// deleteOwnedRenderBindings removes all RenderBindings owned by this target.
843
// Called during Target deletion to trigger GC of any associated RenderArtifacts.
844
func (r *TargetReconciler) deleteOwnedRenderBindings(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
845
        bindingList := &solarv1alpha1.RenderBindingList{}
2✔
846
        if err := r.List(ctx, bindingList,
2✔
847
                client.InNamespace(target.Namespace),
2✔
848
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
849
        ); err != nil {
2✔
UNCOV
850
                return err
×
UNCOV
851
        }
×
852

853
        for i := range bindingList.Items {
3✔
854
                b := &bindingList.Items[i]
1✔
855
                if b.Spec.OwnerName == target.Name && b.Spec.OwnerNamespace == target.Namespace {
2✔
856
                        if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
1✔
UNCOV
857
                                return err
×
UNCOV
858
                        }
×
859
                }
860
        }
861

862
        return nil
2✔
863
}
864

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

881
                return nil
43✔
882
        } else if !apierrors.IsNotFound(err) {
11✔
UNCOV
883
                return err
×
UNCOV
884
        }
×
885

886
        artifact = &solarv1alpha1.RenderArtifact{
11✔
887
                ObjectMeta: metav1.ObjectMeta{
11✔
888
                        Name:      name,
11✔
889
                        Namespace: rt.Namespace,
11✔
890
                },
11✔
891
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
892
                        BaseURL:             rt.Spec.BaseURL,
11✔
893
                        Repository:          rt.Spec.Repository,
11✔
894
                        Tag:                 rt.Spec.Tag,
11✔
895
                        RenderTaskRef:       rt.Name,
11✔
896
                        PushSecretRef:       rt.Spec.PushSecretRef,
11✔
897
                        PushSecretNamespace: pushSecretNamespace,
11✔
898
                        RegistryFlavor:      flavor,
11✔
899
                },
11✔
900
        }
11✔
901

11✔
902
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
UNCOV
903
                return err
×
904
        }
×
905

906
        return nil
11✔
907
}
908

909
// ensureRenderBinding creates a RenderBinding linking this Target to the named
910
// RenderArtifact if one does not already exist. Idempotent.
911
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string) error {
54✔
912
        binding := &solarv1alpha1.RenderBinding{}
54✔
913
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
97✔
914
                return nil
43✔
915
        } else if !apierrors.IsNotFound(err) {
54✔
916
                return err
×
UNCOV
917
        }
×
918

919
        binding = &solarv1alpha1.RenderBinding{
11✔
920
                ObjectMeta: metav1.ObjectMeta{
11✔
921
                        Name:      bindingName,
11✔
922
                        Namespace: target.Namespace,
11✔
923
                },
11✔
924
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
925
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
926
                        OwnerKind:         "Target",
11✔
927
                        OwnerName:         target.Name,
11✔
928
                        OwnerNamespace:    target.Namespace,
11✔
929
                },
11✔
930
        }
11✔
931

11✔
932
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
UNCOV
933
                return err
×
UNCOV
934
        }
×
935

936
        return nil
11✔
937
}
938

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

119✔
943
        var targetNamespace string
119✔
944
        if rel.Spec.TargetNamespace != nil {
238✔
945
                targetNamespace = *rel.Spec.TargetNamespace
119✔
946
        }
119✔
947

948
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
119✔
949
        if err != nil {
129✔
950
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
951
        }
10✔
952

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

109✔
959
        return solarv1alpha1.RenderTaskSpec{
109✔
960
                RendererConfig: solarv1alpha1.RendererConfig{
109✔
961
                        Type: solarv1alpha1.RendererConfigTypeRelease,
109✔
962
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
109✔
963
                                Chart: solarv1alpha1.ChartConfig{
109✔
964
                                        Name:        chartName,
109✔
965
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
109✔
966
                                        Version:     tag,
109✔
967
                                        AppVersion:  tag,
109✔
968
                                },
109✔
969
                                Input: solarv1alpha1.ReleaseInput{
109✔
970
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
109✔
971
                                        Resources:  resolvedResources,
109✔
972
                                        Entrypoint: cv.Spec.Entrypoint,
109✔
973
                                },
109✔
974
                                Values:          rel.Spec.Values,
109✔
975
                                TargetNamespace: targetNamespace,
109✔
976
                        },
109✔
977
                },
109✔
978
                Repository:     repo,
109✔
979
                Tag:            tag,
109✔
980
                BaseURL:        registry.Spec.Hostname,
109✔
981
                PushSecretRef:  registry.Spec.SolarSecretRef,
109✔
982
                FailedJobTTL:   rel.Spec.FailedJobTTL,
109✔
983
                OwnerName:      target.Name,
109✔
984
                OwnerNamespace: target.Namespace,
109✔
985
                OwnerKind:      "Target",
109✔
986
        }, nil
109✔
987
}
988

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

35✔
994
        for _, ri := range releases {
83✔
995
                if ri.uniqueName == "" {
49✔
996
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
997
                }
1✔
998

999
                ref, err := ociname.ParseReference(ri.chartURL)
47✔
1000
                if err != nil {
47✔
UNCOV
1001
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
1002
                }
×
1003

1004
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
47✔
1005
                if err != nil {
47✔
UNCOV
1006
                        return solarv1alpha1.BootstrapInput{}, err
×
UNCOV
1007
                }
×
1008

1009
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
47✔
1010
                        Repository:     strings.TrimPrefix(repo, "oci://"),
47✔
1011
                        Tag:            ref.Identifier(),
47✔
1012
                        PullSecretName: renderRegistryPullSecret,
47✔
1013
                }
47✔
1014
        }
1015

1016
        return solarv1alpha1.BootstrapInput{
34✔
1017
                Releases: resolvedReleases,
34✔
1018
                Userdata: target.Spec.Userdata,
34✔
1019
        }, nil
34✔
1020
}
1021

1022
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
7✔
1023
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
7✔
1024
        if err != nil {
7✔
UNCOV
1025
                return solarv1alpha1.RenderTaskSpec{}, err
×
UNCOV
1026
        }
×
1027

1028
        releaseNames := make([]string, 0, len(releases))
7✔
1029
        for _, ri := range releases {
16✔
1030
                releaseNames = append(releaseNames, ri.name)
9✔
1031
        }
9✔
1032

1033
        sort.Strings(releaseNames)
7✔
1034

7✔
1035
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
7✔
1036
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
1037
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
7✔
1038

7✔
1039
        return solarv1alpha1.RenderTaskSpec{
7✔
1040
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
1041
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
7✔
1042
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
7✔
1043
                                Chart: solarv1alpha1.ChartConfig{
7✔
1044
                                        Name:        chartName,
7✔
1045
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
7✔
1046
                                        Version:     tag,
7✔
1047
                                        AppVersion:  tag,
7✔
1048
                                },
7✔
1049
                                Input: input,
7✔
1050
                        },
7✔
1051
                },
7✔
1052
                Repository:     repo,
7✔
1053
                Tag:            tag,
7✔
1054
                BaseURL:        registry.Spec.Hostname,
7✔
1055
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
1056
                OwnerName:      target.Name,
7✔
1057
                OwnerNamespace: target.Namespace,
7✔
1058
                OwnerKind:      "Target",
7✔
1059
        }, nil
7✔
1060
}
1061

1062
// SetupWithManager sets up the controller with the Manager.
1063
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
1064
        return ctrl.NewControllerManagedBy(mgr).
1✔
1065
                For(&solarv1alpha1.Target{}).
1✔
1066
                Watches(
1✔
1067
                        &solarv1alpha1.ReleaseBinding{},
1✔
1068
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
1069
                ).
1✔
1070
                Watches(
1✔
1071
                        &solarv1alpha1.RenderTask{},
1✔
1072
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
1073
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
1074
                ).
1✔
1075
                Watches(
1✔
1076
                        &solarv1alpha1.Registry{},
1✔
1077
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
1078
                ).
1✔
1079
                Watches(
1✔
1080
                        &solarv1alpha1.RegistryBinding{},
1✔
1081
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
1082
                ).
1✔
1083
                Watches(
1✔
1084
                        &solarv1alpha1.ReferenceGrant{},
1✔
1085
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
1086
                ).
1✔
1087
                Watches(
1✔
1088
                        &solarv1alpha1.Release{},
1✔
1089
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
1090
                ).
1✔
1091
                Complete(r)
1✔
1092
}
1✔
1093

1094
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
1095
// fromNamespace to reference the named registry.
UNCOV
1096
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
UNCOV
1097
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
UNCOV
1098
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
UNCOV
1099
                return false, err
×
1100
        }
×
UNCOV
1101
        for i := range grantList.Items {
×
UNCOV
1102
                grant := &grantList.Items[i]
×
UNCOV
1103
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
1104
                        return true, nil
×
1105
                }
×
1106
        }
1107

UNCOV
1108
        return false, nil
×
1109
}
1110

1111
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
1112
// fromNamespace to reference Registry resources in the grant's namespace.
UNCOV
1113
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
UNCOV
1114
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
UNCOV
1115
}
×
1116

1117
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1118
// Targets that reference it — either in the same namespace or cross-namespace.
1119
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
31✔
1120
        reg, ok := obj.(*solarv1alpha1.Registry)
31✔
1121
        if !ok {
31✔
UNCOV
1122
                return nil
×
UNCOV
1123
        }
×
1124

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

×
UNCOV
1130
                return nil
×
UNCOV
1131
        }
×
1132

1133
        var requests []reconcile.Request
31✔
1134
        for _, t := range targetList.Items {
32✔
1135
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
1✔
1136
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
1✔
UNCOV
1137
                        requests = append(requests, reconcile.Request{
×
UNCOV
1138
                                NamespacedName: types.NamespacedName{
×
UNCOV
1139
                                        Name:      t.Name,
×
UNCOV
1140
                                        Namespace: t.Namespace,
×
UNCOV
1141
                                },
×
UNCOV
1142
                        })
×
UNCOV
1143
                }
×
1144
        }
1145

1146
        // Cross-namespace targets: find namespaces that have been granted access to
1147
        // registries in reg.Namespace, then check their targets.
1148
        grantList := &solarv1alpha1.ReferenceGrantList{}
31✔
1149
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
31✔
UNCOV
1150
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
UNCOV
1151
                return requests
×
UNCOV
1152
        }
×
1153

1154
        for i := range grantList.Items {
31✔
UNCOV
1155
                grant := &grantList.Items[i]
×
1156
                if !grantsRegistryResource(grant) {
×
1157
                        continue
×
1158
                }
UNCOV
1159
                for _, from := range grant.Spec.From {
×
UNCOV
1160
                        if from.Kind != "Target" || from.Group != solarGroup {
×
UNCOV
1161
                                continue
×
1162
                        }
UNCOV
1163
                        crossTargets := &solarv1alpha1.TargetList{}
×
UNCOV
1164
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
UNCOV
1165
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
UNCOV
1166
                                continue
×
1167
                        }
UNCOV
1168
                        for _, t := range crossTargets.Items {
×
UNCOV
1169
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
UNCOV
1170
                                        requests = append(requests, reconcile.Request{
×
UNCOV
1171
                                                NamespacedName: types.NamespacedName{
×
UNCOV
1172
                                                        Name:      t.Name,
×
UNCOV
1173
                                                        Namespace: t.Namespace,
×
UNCOV
1174
                                                },
×
UNCOV
1175
                                        })
×
UNCOV
1176
                                }
×
1177
                        }
1178
                }
1179
        }
1180

1181
        return requests
31✔
1182
}
1183

1184
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
1185
// each bound Registry, and returns a map from registry hostname to
1186
// targetPullSecretName. Registries without a targetPullSecretName are included
1187
// with an empty string (anonymous pull).
1188
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
132✔
1189
        rbList := &solarv1alpha1.RegistryBindingList{}
132✔
1190
        if err := r.List(ctx, rbList,
132✔
1191
                client.InNamespace(target.Namespace),
132✔
1192
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
132✔
1193
        ); err != nil {
132✔
UNCOV
1194
                return nil, err
×
UNCOV
1195
        }
×
1196

1197
        type hostEntry struct {
132✔
1198
                pullSecret  string
132✔
1199
                bindingName string
132✔
1200
        }
132✔
1201

132✔
1202
        lookup := make(map[string]hostEntry, len(rbList.Items))
132✔
1203

132✔
1204
        for _, rb := range rbList.Items {
171✔
1205
                reg := &solarv1alpha1.Registry{}
39✔
1206
                if err := r.Get(ctx, client.ObjectKey{
39✔
1207
                        Name:      rb.Spec.RegistryRef.Name,
39✔
1208
                        Namespace: rb.Namespace,
39✔
1209
                }, reg); err != nil {
49✔
1210
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1211
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1212
                }
10✔
1213

1214
                host := strings.ToLower(reg.Spec.Hostname)
29✔
1215
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
39✔
1216
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
10✔
1217
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1218
                }
10✔
1219

1220
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
19✔
1221
        }
1222

1223
        result := make(map[string]string, len(lookup))
112✔
1224
        for host, entry := range lookup {
121✔
1225
                result[host] = entry.pullSecret
9✔
1226
        }
9✔
1227

1228
        return result, nil
112✔
1229
}
1230

1231
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1232
// for the referenced Target.
1233
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
7✔
1234
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
7✔
1235
        if !ok {
7✔
UNCOV
1236
                return nil
×
UNCOV
1237
        }
×
1238

1239
        if rb.Spec.TargetRef.Name == "" {
7✔
UNCOV
1240
                return nil
×
UNCOV
1241
        }
×
1242

1243
        return []reconcile.Request{
7✔
1244
                {
7✔
1245
                        NamespacedName: types.NamespacedName{
7✔
1246
                                Name:      rb.Spec.TargetRef.Name,
7✔
1247
                                Namespace: rb.Namespace,
7✔
1248
                        },
7✔
1249
                },
7✔
1250
        }
7✔
1251
}
1252

1253
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1254
// either because the grant controls Registry access (Target → Registry) or because
1255
// it controls ComponentVersion access (Release → ComponentVersion).
1256
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
13✔
1257
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
13✔
1258
        if !ok {
13✔
UNCOV
1259
                return nil
×
UNCOV
1260
        }
×
1261

1262
        var requests []reconcile.Request
13✔
1263

13✔
1264
        if grantsRegistryResource(grant) {
13✔
1265
                for _, from := range grant.Spec.From {
×
1266
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1267
                                continue
×
1268
                        }
UNCOV
1269
                        targets := &solarv1alpha1.TargetList{}
×
UNCOV
1270
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
UNCOV
1271
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
UNCOV
1272
                                continue
×
1273
                        }
UNCOV
1274
                        for _, t := range targets.Items {
×
UNCOV
1275
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
1276
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
1277
                                        requests = append(requests, reconcile.Request{
×
UNCOV
1278
                                                NamespacedName: types.NamespacedName{
×
UNCOV
1279
                                                        Name:      t.Name,
×
UNCOV
1280
                                                        Namespace: t.Namespace,
×
UNCOV
1281
                                                },
×
UNCOV
1282
                                        })
×
UNCOV
1283
                                }
×
1284
                        }
1285
                }
1286
        }
1287

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

1322
        if grantsReleaseBindingToTargetResource(grant) {
17✔
1323
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1324
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1325
                // the new or removed cross-namespace ReleaseBindings.
4✔
1326
                targets := &solarv1alpha1.TargetList{}
4✔
1327
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
UNCOV
1328
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1329
                } else {
4✔
1330
                        for _, t := range targets.Items {
8✔
1331
                                requests = append(requests, reconcile.Request{
4✔
1332
                                        NamespacedName: types.NamespacedName{
4✔
1333
                                                Name:      t.Name,
4✔
1334
                                                Namespace: t.Namespace,
4✔
1335
                                        },
4✔
1336
                                })
4✔
1337
                        }
4✔
1338
                }
1339
        }
1340

1341
        return requests
13✔
1342
}
1343

1344
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1345
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
13✔
1346
        for _, t := range grant.Spec.To {
26✔
1347
                if t.Kind == "Registry" && t.Group == solarGroup {
13✔
UNCOV
1348
                        return true
×
UNCOV
1349
                }
×
1350
        }
1351

1352
        return false
13✔
1353
}
1354

1355
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1356
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1357
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
24✔
1358
        hasReleaseBindingFrom := false
24✔
1359
        for _, f := range grant.Spec.From {
48✔
1360
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
39✔
1361
                        hasReleaseBindingFrom = true
15✔
1362
                        break
15✔
1363
                }
1364
        }
1365
        if !hasReleaseBindingFrom {
33✔
1366
                return false
9✔
1367
        }
9✔
1368
        for _, t := range grant.Spec.To {
30✔
1369
                if t.Kind == "Target" && t.Group == solarGroup {
30✔
1370
                        return true
15✔
1371
                }
15✔
1372
        }
1373

UNCOV
1374
        return false
×
1375
}
1376

1377
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1378
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1379
// a ReferenceGrant in target's namespace.
1380
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
112✔
1381
        grantList := &solarv1alpha1.ReferenceGrantList{}
112✔
1382
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
112✔
UNCOV
1383
                return nil, err
×
UNCOV
1384
        }
×
1385

1386
        seen := make(map[string]struct{})
112✔
1387
        var result []solarv1alpha1.ReleaseBinding
112✔
1388
        for i := range grantList.Items {
123✔
1389
                grant := &grantList.Items[i]
11✔
1390
                if !grantsReleaseBindingToTargetResource(grant) {
11✔
UNCOV
1391
                        continue
×
1392
                }
1393
                for _, from := range grant.Spec.From {
22✔
1394
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
11✔
UNCOV
1395
                                continue
×
1396
                        }
1397
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
11✔
1398
                        if err := r.List(ctx, crossBindings,
11✔
1399
                                client.InNamespace(from.Namespace),
11✔
1400
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
11✔
1401
                        ); err != nil {
11✔
UNCOV
1402
                                return nil, err
×
UNCOV
1403
                        }
×
1404
                        for _, rb := range crossBindings.Items {
19✔
1405
                                if rb.Spec.TargetNamespace != target.Namespace {
8✔
UNCOV
1406
                                        continue
×
1407
                                }
1408
                                key := rb.Namespace + "/" + rb.Name
8✔
1409
                                if _, exists := seen[key]; exists {
9✔
1410
                                        continue
1✔
1411
                                }
1412
                                seen[key] = struct{}{}
7✔
1413
                                result = append(result, rb)
7✔
1414
                        }
1415
                }
1416
        }
1417

1418
        return result, nil
112✔
1419
}
1420

1421
// mapReleaseToTargets maps a Release event to reconcile requests for all
1422
// Targets that are bound to the release via ReleaseBindings.
1423
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
103✔
1424
        rel, ok := obj.(*solarv1alpha1.Release)
103✔
1425
        if !ok {
103✔
UNCOV
1426
                return nil
×
UNCOV
1427
        }
×
1428

1429
        bindingList := &solarv1alpha1.ReleaseBindingList{}
103✔
1430
        if err := r.List(ctx, bindingList,
103✔
1431
                client.InNamespace(rel.Namespace),
103✔
1432
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
103✔
1433
        ); err != nil {
103✔
UNCOV
1434
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
UNCOV
1435

×
UNCOV
1436
                return nil
×
UNCOV
1437
        }
×
1438

1439
        seen := map[string]struct{}{}
103✔
1440
        var requests []reconcile.Request
103✔
1441

103✔
1442
        for _, rb := range bindingList.Items {
105✔
1443
                targetNs := rb.Namespace
2✔
1444
                if rb.Spec.TargetNamespace != "" {
2✔
UNCOV
1445
                        targetNs = rb.Spec.TargetNamespace
×
UNCOV
1446
                }
×
1447

1448
                key := targetNs + "/" + rb.Spec.TargetRef.Name
2✔
1449
                if _, ok := seen[key]; ok {
2✔
UNCOV
1450
                        continue
×
1451
                }
1452

1453
                seen[key] = struct{}{}
2✔
1454
                requests = append(requests, reconcile.Request{
2✔
1455
                        NamespacedName: types.NamespacedName{
2✔
1456
                                Name:      rb.Spec.TargetRef.Name,
2✔
1457
                                Namespace: targetNs,
2✔
1458
                        },
2✔
1459
                })
2✔
1460
        }
1461

1462
        return requests
103✔
1463
}
1464

1465
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
39✔
1466
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
39✔
1467
        if !ok || rb.Spec.TargetRef.Name == "" {
39✔
UNCOV
1468
                return nil
×
UNCOV
1469
        }
×
1470

1471
        targetNs := rb.Namespace
39✔
1472
        if rb.Spec.TargetNamespace != "" {
48✔
1473
                targetNs = rb.Spec.TargetNamespace
9✔
1474
        }
9✔
1475

1476
        return []reconcile.Request{
39✔
1477
                {
39✔
1478
                        NamespacedName: types.NamespacedName{
39✔
1479
                                Name:      rb.Spec.TargetRef.Name,
39✔
1480
                                Namespace: targetNs,
39✔
1481
                        },
39✔
1482
                },
39✔
1483
        }
39✔
1484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc