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

opendefensecloud / solution-arsenal / 30623048857

31 Jul 2026 10:17AM UTC coverage: 81.918% (-0.04%) from 81.954%
30623048857

push

github

web-flow
fix: restructured renderartifact and -binding to solve a known problem (#724)

## What
Changed the structure of RenderArtifacts and -bindings to reference
Registries instead of having a copy of the secret
Closes #616 

## Why
RenderArtifacts had possibly stale secrets with the old implementation,
and secrets would need to be repinned, dependent on remaining
ArtifatBindings etc., which was convoluted and not ideal for the simple
reason that we don't want the registry secrets or other sensitive data
copied in too many ressources.

## Testing
make test

## Notes for reviewers
RenderArtifact lost some Registry-Specific Fields, and RenderArtifact as
well as ArtifatBindings got a RegistryRef field.

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


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

* **New Features**
* Render artifacts now reference a Registry for credentials and
transport settings.
* Render bindings preserve Registry references and support automatic
artifact re-pinning.
* Cross-namespace Registry access is supported through explicit
authorization grants.
  * Registry changes are propagated to existing artifacts and bindings.

* **Bug Fixes**
* Improved OCI cleanup authentication, host validation, and
insecure-transport handling.
* Unauthorized cleanup now reports a clear authentication failure
status.

* **Documentation**
* Updated API references, user guidance, and configuration examples for
Registry references and access grants.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

92 of 102 new or added lines in 2 files covered. (90.2%)

7 existing lines in 3 files now uncovered.

5246 of 6404 relevant lines covered (81.92%)

29.47 hits per line

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

78.0
/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
        "slices"
11
        "sort"
12
        "strings"
13
        "time"
14

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

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

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

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

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

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

63
type TargetReconciler struct {
64
        client.Client
65
        Scheme    *runtime.Scheme
66
        Recorder  events.EventRecorder
67
        APIReader client.Reader
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;update;patch
83
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries/finalizers,verbs=update
84
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch
85
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registrybindings,verbs=get;list;watch
86
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
87
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
88
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list;watch
89
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
90
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderartifacts,verbs=get;list;watch;create;update;patch
91
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=renderbindings,verbs=get;list;watch;create;update;patch;delete
92
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
93

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

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

498✔
101
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
722✔
102
                return ctrl.Result{}, nil
224✔
103
        }
224✔
104

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

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

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

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

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

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

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

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

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

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

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

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

181
                return ctrl.Result{}, nil
38✔
182
        }
183

184
        // Resolve render registry — supports cross-namespace via ReferenceGrant
185
        registryNamespace := target.Namespace
182✔
186
        if target.Spec.RenderRegistryRef.Namespace != "" {
186✔
187
                registryNamespace = target.Spec.RenderRegistryRef.Namespace
4✔
188
        }
4✔
189

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

303
                return ctrl.Result{}, nil
10✔
304
        }
305

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

101✔
309
        pendingDeps := false
101✔
310

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

×
321
                                continue
×
322
                        }
323

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

327
                cv := &solarv1alpha1.ComponentVersion{}
127✔
328
                cvNamespace := rel.Namespace
127✔
329
                if rel.Spec.ComponentVersionRef.Namespace != "" {
127✔
330
                        cvNamespace = rel.Spec.ComponentVersionRef.Namespace
×
331
                }
×
332

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

×
349
                                continue
×
350
                        }
351
                }
352

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

×
361
                                continue
×
362
                        }
363

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

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

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

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

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

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

101✔
397
        for i, ri := range releases {
219✔
398
                rt := &solarv1alpha1.RenderTask{}
118✔
399
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
118✔
400

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

32✔
529
        needsNewBootstrap := false
32✔
530

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

545
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
27✔
546
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
29✔
547
                        bootstrapVersion++
2✔
548
                        needsNewBootstrap = true
2✔
549
                }
2✔
550
        }
551

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

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

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

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

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

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

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

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

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

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

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

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

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

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

662
        return nil
435✔
663
}
664

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

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

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

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

109✔
686
        for i, ri := range releases {
250✔
687
                uname := effectiveUniqueName(ri.release, ri.cv)
141✔
688
                releases[i].uniqueName = uname
141✔
689
                namedGroups[uname] = append(namedGroups[uname], releases[i])
141✔
690
        }
141✔
691

692
        var accepted []releaseInfo
109✔
693

109✔
694
        var skipped []string
109✔
695

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

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

705
        uniqueNames := make([]string, 0, len(namedGroups))
109✔
706
        for k := range namedGroups {
241✔
707
                uniqueNames = append(uniqueNames, k)
132✔
708
        }
132✔
709

710
        sort.Strings(uniqueNames)
109✔
711

109✔
712
        for _, uniqueName := range uniqueNames {
241✔
713
                group := namedGroups[uniqueName]
132✔
714
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
141✔
715

716
                accepted = append(accepted, group[0])
132✔
717

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

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

731
        resolved := make([]releaseInfo, 0, len(accepted))
109✔
732

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

1✔
744
                                continue
1✔
745
                        }
746

747
                        riSelector = sel
6✔
748
                }
749

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

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

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

778
        return resolved, skipped
109✔
779
}
780

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

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

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

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

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

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

814
        return nil
19✔
815
}
816

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

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

835
        return nil
2✔
836
}
837

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

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

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

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

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

864
        return nil
19✔
865
}
866

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

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

884
        return nil
2✔
885
}
886

887
// ensureRenderArtifact creates a RenderArtifact for the given RenderTask's OCI coordinates
888
// if one does not already exist. Idempotent: if it already exists (possibly created by
889
// another Target reconciling the same shared artifact), this is a no-op
890
// RegistryRef for an existing artifact is kept in sync separately, by RenderArtifactReconciler
891
// re-pinning from RenderBinding snapshots (see ensureRenderBinding below).
892
func (r *TargetReconciler) ensureRenderArtifact(ctx context.Context, name string, rt *solarv1alpha1.RenderTask, registryRef solarv1alpha1.ObjectReference) error {
55✔
893
        artifact := &solarv1alpha1.RenderArtifact{}
55✔
894
        if err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: rt.Namespace}, artifact); err == nil {
99✔
895
                if !artifact.DeletionTimestamp.IsZero() {
44✔
896
                        // The artifact is terminating (OCI cleanup in progress). Creating a binding
×
897
                        // against it would race with the finalizer. Requeue and wait for full deletion.
×
898
                        return fmt.Errorf("RenderArtifact %s/%s is terminating; requeuing", rt.Namespace, name)
×
899
                }
×
900

901
                return nil
44✔
902
        } else if !apierrors.IsNotFound(err) {
11✔
903
                return err
×
904
        }
×
905

906
        artifact = &solarv1alpha1.RenderArtifact{
11✔
907
                ObjectMeta: metav1.ObjectMeta{
11✔
908
                        Name:      name,
11✔
909
                        Namespace: rt.Namespace,
11✔
910
                },
11✔
911
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
912
                        BaseURL:       rt.Spec.BaseURL,
11✔
913
                        Repository:    rt.Spec.Repository,
11✔
914
                        Tag:           rt.Spec.Tag,
11✔
915
                        RenderTaskRef: rt.Name,
11✔
916
                        RegistryRef:   &registryRef,
11✔
917
                },
11✔
918
        }
11✔
919

11✔
920
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
921
                return err
×
922
        }
×
923

924
        return nil
11✔
925
}
926

927
// ensureRenderBinding creates a RenderBinding linking this Target to the named
928
// RenderArtifact if one does not already exist, and keeps an existing binding's Registry
929
// snapshot in sync with the Target's current reference. Idempotent.
930
//
931
// The binding snapshots the Registry this Target currently resolves (registryRef) so
932
// RenderArtifactReconciler can re-pin the shared RenderArtifact's RegistryRef from a
933
// surviving binding whenever another binding referencing the same artifact is removed.
934
// A snapshot left behind after Target.Spec.RenderRegistryRef changes outlives the Registry
935
// it names: once that Registry or its Secret is gone, re-pinning hands the artifact a
936
// reference cleanup cannot resolve and the finalizer stays stuck. Patching here re-triggers
937
// RenderArtifactReconciler (it watches RenderBindings), which re-pins the artifact.
938
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string, registryRef solarv1alpha1.ObjectReference) error {
55✔
939
        binding := &solarv1alpha1.RenderBinding{}
55✔
940
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
99✔
941
                if registryRefEqual(binding.Spec.RegistryRef, &registryRef) {
87✔
942
                        return nil
43✔
943
                }
43✔
944

945
                latest := binding.DeepCopy()
1✔
946
                latest.Spec.RegistryRef = &registryRef
1✔
947

1✔
948
                return r.Patch(ctx, latest, client.MergeFrom(binding))
1✔
949
        } else if !apierrors.IsNotFound(err) {
11✔
950
                return err
×
951
        }
×
952

953
        binding = &solarv1alpha1.RenderBinding{
11✔
954
                ObjectMeta: metav1.ObjectMeta{
11✔
955
                        Name:      bindingName,
11✔
956
                        Namespace: target.Namespace,
11✔
957
                },
11✔
958
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
959
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
960
                        OwnerKind:         "Target",
11✔
961
                        OwnerName:         target.Name,
11✔
962
                        OwnerNamespace:    target.Namespace,
11✔
963
                        RegistryRef:       &registryRef,
11✔
964
                },
11✔
965
        }
11✔
966

11✔
967
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
968
                return err
×
969
        }
×
970

971
        return nil
11✔
972
}
973

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

118✔
978
        var targetNamespace string
118✔
979
        if rel.Spec.TargetNamespace != nil {
236✔
980
                targetNamespace = *rel.Spec.TargetNamespace
118✔
981
        }
118✔
982

983
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
118✔
984
        if err != nil {
128✔
985
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
986
        }
10✔
987

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

108✔
994
        return solarv1alpha1.RenderTaskSpec{
108✔
995
                RendererConfig: solarv1alpha1.RendererConfig{
108✔
996
                        Type: solarv1alpha1.RendererConfigTypeRelease,
108✔
997
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
108✔
998
                                Chart: solarv1alpha1.ChartConfig{
108✔
999
                                        Name:        chartName,
108✔
1000
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
108✔
1001
                                        Version:     tag,
108✔
1002
                                        AppVersion:  tag,
108✔
1003
                                },
108✔
1004
                                Input: solarv1alpha1.ReleaseInput{
108✔
1005
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
108✔
1006
                                        Resources:  resolvedResources,
108✔
1007
                                        Entrypoint: cv.Spec.Entrypoint,
108✔
1008
                                },
108✔
1009
                                Values:          rel.Spec.Values,
108✔
1010
                                TargetNamespace: targetNamespace,
108✔
1011
                        },
108✔
1012
                },
108✔
1013
                Repository:     repo,
108✔
1014
                Tag:            tag,
108✔
1015
                BaseURL:        registry.Spec.Hostname,
108✔
1016
                PlainHTTP:      registry.Spec.PlainHTTP,
108✔
1017
                PushSecretRef:  registry.Spec.SolarSecretRef,
108✔
1018
                FailedJobTTL:   rel.Spec.FailedJobTTL,
108✔
1019
                OwnerName:      target.Name,
108✔
1020
                OwnerNamespace: target.Namespace,
108✔
1021
                OwnerKind:      "Target",
108✔
1022
        }, nil
108✔
1023
}
1024

1025
// buildBootstrapInput constructs the desired BootstrapInput from the current
1026
// target and resolved releases. Used for both comparison and spec construction.
1027
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string, insecure bool) (solarv1alpha1.BootstrapInput, error) {
39✔
1028
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
39✔
1029

39✔
1030
        for _, ri := range releases {
91✔
1031
                if ri.uniqueName == "" {
53✔
1032
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
1033
                }
1✔
1034

1035
                trimmedRef := strings.TrimPrefix(ri.chartURL, "oci://")
51✔
1036
                ref, err := ociname.ParseReference(trimmedRef)
51✔
1037
                if err != nil {
51✔
1038
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
1039
                }
×
1040

1041
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
51✔
1042
                        Repository:     ref.Context().String(),
51✔
1043
                        Tag:            ref.Identifier(),
51✔
1044
                        PullSecretName: renderRegistryPullSecret,
51✔
1045
                        Insecure:       insecure,
51✔
1046
                }
51✔
1047
        }
1048

1049
        return solarv1alpha1.BootstrapInput{
38✔
1050
                Releases: resolvedReleases,
38✔
1051
                Userdata: target.Spec.Userdata,
38✔
1052
        }, nil
38✔
1053
}
1054

1055
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
7✔
1056
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName, registry.Spec.PlainHTTP)
7✔
1057
        if err != nil {
7✔
1058
                return solarv1alpha1.RenderTaskSpec{}, err
×
1059
        }
×
1060

1061
        releaseNames := make([]string, 0, len(releases))
7✔
1062
        for _, ri := range releases {
16✔
1063
                releaseNames = append(releaseNames, ri.name)
9✔
1064
        }
9✔
1065

1066
        sort.Strings(releaseNames)
7✔
1067

7✔
1068
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
7✔
1069
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
1070
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
7✔
1071

7✔
1072
        return solarv1alpha1.RenderTaskSpec{
7✔
1073
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
1074
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
7✔
1075
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
7✔
1076
                                Chart: solarv1alpha1.ChartConfig{
7✔
1077
                                        Name:        chartName,
7✔
1078
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
7✔
1079
                                        Version:     tag,
7✔
1080
                                        AppVersion:  tag,
7✔
1081
                                },
7✔
1082
                                Input: input,
7✔
1083
                        },
7✔
1084
                },
7✔
1085
                Repository:     repo,
7✔
1086
                Tag:            tag,
7✔
1087
                BaseURL:        registry.Spec.Hostname,
7✔
1088
                PlainHTTP:      registry.Spec.PlainHTTP,
7✔
1089
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
1090
                OwnerName:      target.Name,
7✔
1091
                OwnerNamespace: target.Namespace,
7✔
1092
                OwnerKind:      "Target",
7✔
1093
        }, nil
7✔
1094
}
1095

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

1128
// registryGranted checks whether a ReferenceGrant in registryNamespace permits a
1129
// resource of fromKind in fromNamespace to reference the named registry.
1130
// fromKind is the kind that is actually resolving the reference: each kind needs its
1131
// own grant subject, so a grant written for one kind never authorizes another.
1132
func registryGranted(ctx context.Context, reader client.Reader, registryNamespace, fromKind, fromNamespace string) (bool, error) {
11✔
1133
        grantList := &solarv1alpha1.ReferenceGrantList{}
11✔
1134
        if err := reader.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
12✔
1135
                return false, err
1✔
1136
        }
1✔
1137
        for i := range grantList.Items {
16✔
1138
                grant := &grantList.Items[i]
6✔
1139
                if grantPermitsRegistryAccess(grant, fromKind, fromNamespace) {
10✔
1140
                        return true, nil
4✔
1141
                }
4✔
1142
        }
1143

1144
        return false, nil
6✔
1145
}
1146

1147
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a resource of
1148
// fromKind in fromNamespace to reference Registry resources in the grant's namespace.
1149
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromKind, fromNamespace string) bool {
10✔
1150
        return grantPermits(grant, solarGroup, fromKind, fromNamespace, solarGroup, "Registry")
10✔
1151
}
10✔
1152

1153
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1154
// Targets that reference it — either in the same namespace or cross-namespace.
1155
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
235✔
1156
        reg, ok := obj.(*solarv1alpha1.Registry)
235✔
1157
        if !ok {
235✔
1158
                return nil
×
1159
        }
×
1160

1161
        // Same-namespace targets
1162
        targetList := &solarv1alpha1.TargetList{}
235✔
1163
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
235✔
1164
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
1165

×
1166
                return nil
×
1167
        }
×
1168

1169
        var requests []reconcile.Request
235✔
1170
        for _, t := range targetList.Items {
306✔
1171
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
71✔
1172
                        (t.Spec.RenderRegistryRef.Namespace == "" || t.Spec.RenderRegistryRef.Namespace == reg.Namespace) {
128✔
1173
                        requests = append(requests, reconcile.Request{
57✔
1174
                                NamespacedName: types.NamespacedName{
57✔
1175
                                        Name:      t.Name,
57✔
1176
                                        Namespace: t.Namespace,
57✔
1177
                                },
57✔
1178
                        })
57✔
1179
                }
57✔
1180
        }
1181

1182
        // Cross-namespace targets: find namespaces that have been granted access to
1183
        // registries in reg.Namespace, then check their targets.
1184
        grantList := &solarv1alpha1.ReferenceGrantList{}
235✔
1185
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
235✔
1186
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
1187
                return requests
×
1188
        }
×
1189

1190
        for i := range grantList.Items {
258✔
1191
                grant := &grantList.Items[i]
23✔
1192
                if !grantsRegistryResource(grant) {
43✔
1193
                        continue
20✔
1194
                }
1195
                for _, from := range grant.Spec.From {
6✔
1196
                        if from.Kind != "Target" || from.Group != solarGroup {
3✔
1197
                                continue
×
1198
                        }
1199
                        crossTargets := &solarv1alpha1.TargetList{}
3✔
1200
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
3✔
1201
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
1202
                                continue
×
1203
                        }
1204
                        for _, t := range crossTargets.Items {
6✔
1205
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryRef.Namespace == reg.Namespace {
6✔
1206
                                        requests = append(requests, reconcile.Request{
3✔
1207
                                                NamespacedName: types.NamespacedName{
3✔
1208
                                                        Name:      t.Name,
3✔
1209
                                                        Namespace: t.Namespace,
3✔
1210
                                                },
3✔
1211
                                        })
3✔
1212
                                }
3✔
1213
                        }
1214
                }
1215
        }
1216

1217
        return requests
235✔
1218
}
1219

1220
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
1221
// each bound Registry, and returns a map from registry hostname to
1222
// targetPullSecretName. Registries without a targetPullSecretName are included
1223
// with an empty string (anonymous pull).
1224
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
133✔
1225
        rbList := &solarv1alpha1.RegistryBindingList{}
133✔
1226
        if err := r.List(ctx, rbList,
133✔
1227
                client.InNamespace(target.Namespace),
133✔
1228
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
133✔
1229
        ); err != nil {
133✔
1230
                return nil, err
×
1231
        }
×
1232

1233
        type hostEntry struct {
133✔
1234
                pullSecret  string
133✔
1235
                bindingName string
133✔
1236
        }
133✔
1237

133✔
1238
        lookup := make(map[string]hostEntry, len(rbList.Items))
133✔
1239

133✔
1240
        for _, rb := range rbList.Items {
176✔
1241
                reg := &solarv1alpha1.Registry{}
43✔
1242
                if err := r.Get(ctx, client.ObjectKey{
43✔
1243
                        Name:      rb.Spec.RegistryRef.Name,
43✔
1244
                        Namespace: rb.Namespace,
43✔
1245
                }, reg); err != nil {
53✔
1246
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1247
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1248
                }
10✔
1249

1250
                host := strings.ToLower(reg.Spec.Hostname)
33✔
1251
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
45✔
1252
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
12✔
1253
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
12✔
1254
                }
12✔
1255

1256
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
21✔
1257
        }
1258

1259
        result := make(map[string]string, len(lookup))
111✔
1260
        for host, entry := range lookup {
120✔
1261
                result[host] = entry.pullSecret
9✔
1262
        }
9✔
1263

1264
        return result, nil
111✔
1265
}
1266

1267
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1268
// for the referenced Target.
1269
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
66✔
1270
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
66✔
1271
        if !ok {
66✔
1272
                return nil
×
1273
        }
×
1274

1275
        if rb.Spec.TargetRef.Name == "" {
66✔
1276
                return nil
×
1277
        }
×
1278

1279
        return []reconcile.Request{
66✔
1280
                {
66✔
1281
                        NamespacedName: types.NamespacedName{
66✔
1282
                                Name:      rb.Spec.TargetRef.Name,
66✔
1283
                                Namespace: rb.Namespace,
66✔
1284
                        },
66✔
1285
                },
66✔
1286
        }
66✔
1287
}
1288

1289
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1290
// either because the grant controls Registry access (Target → Registry) or because
1291
// it controls ComponentVersion access (Release → ComponentVersion).
1292
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
18✔
1293
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
18✔
1294
        if !ok {
18✔
1295
                return nil
×
1296
        }
×
1297

1298
        var requests []reconcile.Request
18✔
1299

18✔
1300
        if grantsRegistryResource(grant) {
23✔
1301
                for _, from := range grant.Spec.From {
10✔
1302
                        if from.Kind != "Target" || from.Group != solarGroup {
6✔
1303
                                continue
1✔
1304
                        }
1305
                        targets := &solarv1alpha1.TargetList{}
4✔
1306
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
1307
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1308
                                continue
×
1309
                        }
1310
                        for _, t := range targets.Items {
6✔
1311
                                // Enqueue targets that reference a registry specifically in the grant's namespace
2✔
1312
                                if t.Spec.RenderRegistryRef.Namespace == grant.Namespace {
4✔
1313
                                        requests = append(requests, reconcile.Request{
2✔
1314
                                                NamespacedName: types.NamespacedName{
2✔
1315
                                                        Name:      t.Name,
2✔
1316
                                                        Namespace: t.Namespace,
2✔
1317
                                                },
2✔
1318
                                        })
2✔
1319
                                }
2✔
1320
                        }
1321
                }
1322
        }
1323

1324
        if grantsComponentVersionResource(grant) {
23✔
1325
                seen := map[string]struct{}{}
5✔
1326
                for _, from := range grant.Spec.From {
10✔
1327
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
1328
                                continue
×
1329
                        }
1330
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
1331
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
1332
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1333
                                continue
×
1334
                        }
1335
                        for _, rb := range bindings.Items {
6✔
1336
                                if rb.Spec.TargetRef.Name == "" {
1✔
1337
                                        continue
×
1338
                                }
1339
                                targetNs := rb.Namespace
1✔
1340
                                if rb.Spec.TargetRef.Namespace != "" {
2✔
1341
                                        targetNs = rb.Spec.TargetRef.Namespace
1✔
1342
                                }
1✔
1343
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1344
                                if _, ok := seen[key]; ok {
1✔
1345
                                        continue
×
1346
                                }
1347
                                seen[key] = struct{}{}
1✔
1348
                                requests = append(requests, reconcile.Request{
1✔
1349
                                        NamespacedName: types.NamespacedName{
1✔
1350
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1351
                                                Namespace: targetNs,
1✔
1352
                                        },
1✔
1353
                                })
1✔
1354
                        }
1355
                }
1356
        }
1357

1358
        if grantsReleaseBindingToTargetResource(grant) {
22✔
1359
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1360
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1361
                // the new or removed cross-namespace ReleaseBindings.
4✔
1362
                targets := &solarv1alpha1.TargetList{}
4✔
1363
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1364
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1365
                } else {
4✔
1366
                        for _, t := range targets.Items {
8✔
1367
                                requests = append(requests, reconcile.Request{
4✔
1368
                                        NamespacedName: types.NamespacedName{
4✔
1369
                                                Name:      t.Name,
4✔
1370
                                                Namespace: t.Namespace,
4✔
1371
                                        },
4✔
1372
                                })
4✔
1373
                        }
4✔
1374
                }
1375
        }
1376

1377
        return requests
18✔
1378
}
1379

1380
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1381
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
43✔
1382
        for _, t := range grant.Spec.To {
86✔
1383
                if t.Kind == "Registry" && t.Group == solarGroup {
52✔
1384
                        return true
9✔
1385
                }
9✔
1386
        }
1387

1388
        return false
34✔
1389
}
1390

1391
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1392
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1393
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
30✔
1394
        hasReleaseBindingFrom := false
30✔
1395
        for _, f := range grant.Spec.From {
60✔
1396
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
46✔
1397
                        hasReleaseBindingFrom = true
16✔
1398
                        break
16✔
1399
                }
1400
        }
1401
        if !hasReleaseBindingFrom {
44✔
1402
                return false
14✔
1403
        }
14✔
1404
        for _, t := range grant.Spec.To {
32✔
1405
                if t.Kind == "Target" && t.Group == solarGroup {
32✔
1406
                        return true
16✔
1407
                }
16✔
1408
        }
1409

1410
        return false
×
1411
}
1412

1413
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1414
// that reference target via spec.targetRef.name + spec.targetRef.namespace, authorized by
1415
// a ReferenceGrant in target's namespace.
1416
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
111✔
1417
        grantList := &solarv1alpha1.ReferenceGrantList{}
111✔
1418
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
111✔
1419
                return nil, err
×
1420
        }
×
1421

1422
        seen := make(map[string]struct{})
111✔
1423
        var result []solarv1alpha1.ReleaseBinding
111✔
1424
        for i := range grantList.Items {
123✔
1425
                grant := &grantList.Items[i]
12✔
1426
                if !grantsReleaseBindingToTargetResource(grant) {
12✔
1427
                        continue
×
1428
                }
1429
                for _, from := range grant.Spec.From {
24✔
1430
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
12✔
1431
                                continue
×
1432
                        }
1433
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
12✔
1434
                        if err := r.List(ctx, crossBindings,
12✔
1435
                                client.InNamespace(from.Namespace),
12✔
1436
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
12✔
1437
                        ); err != nil {
12✔
1438
                                return nil, err
×
1439
                        }
×
1440
                        for _, rb := range crossBindings.Items {
24✔
1441
                                if rb.Spec.TargetRef.Namespace != target.Namespace {
12✔
1442
                                        continue
×
1443
                                }
1444
                                key := rb.Namespace + "/" + rb.Name
12✔
1445
                                if _, exists := seen[key]; exists {
15✔
1446
                                        continue
3✔
1447
                                }
1448
                                seen[key] = struct{}{}
9✔
1449
                                result = append(result, rb)
9✔
1450
                        }
1451
                }
1452
        }
1453

1454
        return result, nil
111✔
1455
}
1456

1457
// mapReleaseToTargets maps a Release event to reconcile requests for all
1458
// Targets that are bound to the release via ReleaseBindings.
1459
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
428✔
1460
        rel, ok := obj.(*solarv1alpha1.Release)
428✔
1461
        if !ok {
428✔
1462
                return nil
×
1463
        }
×
1464

1465
        bindingList := &solarv1alpha1.ReleaseBindingList{}
428✔
1466
        if err := r.List(ctx, bindingList,
428✔
1467
                client.InNamespace(rel.Namespace),
428✔
1468
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
428✔
1469
        ); err != nil {
428✔
1470
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1471

×
1472
                return nil
×
1473
        }
×
1474

1475
        seen := map[string]struct{}{}
428✔
1476
        var requests []reconcile.Request
428✔
1477

428✔
1478
        for _, rb := range bindingList.Items {
628✔
1479
                targetNs := rb.Namespace
200✔
1480
                if rb.Spec.TargetRef.Namespace != "" {
200✔
1481
                        targetNs = rb.Spec.TargetRef.Namespace
×
1482
                }
×
1483

1484
                key := targetNs + "/" + rb.Spec.TargetRef.Name
200✔
1485
                if _, ok := seen[key]; ok {
204✔
1486
                        continue
4✔
1487
                }
1488

1489
                seen[key] = struct{}{}
196✔
1490
                requests = append(requests, reconcile.Request{
196✔
1491
                        NamespacedName: types.NamespacedName{
196✔
1492
                                Name:      rb.Spec.TargetRef.Name,
196✔
1493
                                Namespace: targetNs,
196✔
1494
                        },
196✔
1495
                })
196✔
1496
        }
1497

1498
        return requests
428✔
1499
}
1500

1501
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
235✔
1502
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
235✔
1503
        if !ok || rb.Spec.TargetRef.Name == "" {
235✔
1504
                return nil
×
1505
        }
×
1506

1507
        targetNs := rb.Namespace
235✔
1508
        if rb.Spec.TargetRef.Namespace != "" {
252✔
1509
                targetNs = rb.Spec.TargetRef.Namespace
17✔
1510
        }
17✔
1511

1512
        return []reconcile.Request{
235✔
1513
                {
235✔
1514
                        NamespacedName: types.NamespacedName{
235✔
1515
                                Name:      rb.Spec.TargetRef.Name,
235✔
1516
                                Namespace: targetNs,
235✔
1517
                        },
235✔
1518
                },
235✔
1519
        }
235✔
1520
}
1521

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