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

opendefensecloud / solution-arsenal / 30900945925

04 Aug 2026 10:30AM UTC coverage: 81.924% (-0.1%) from 82.027%
30900945925

Pull #730

github

web-flow
Merge 716833126 into fa9581050
Pull Request #730: fix: ignore terminating renderartifacts

55 of 74 new or added lines in 2 files covered. (74.32%)

7 existing lines in 2 files now uncovered.

5298 of 6467 relevant lines covered (81.92%)

34.37 hits per line

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

78.14
/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) {
500✔
97
        log := ctrl.LoggerFrom(ctx)
500✔
98

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

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

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

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

115
        // Handle deletion
116
        if !target.DeletionTimestamp.IsZero() {
229✔
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) {
267✔
168
                latest := &solarv1alpha1.Target{}
42✔
169
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
42✔
170
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
171
                }
×
172

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

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

184
        // Resolve render registry — supports cross-namespace via ReferenceGrant
185
        registryNamespace := target.Namespace
183✔
186
        if target.Spec.RenderRegistryRef.Namespace != "" {
187✔
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 {
187✔
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{}
181✔
209
        if err := r.Get(ctx, client.ObjectKey{
181✔
210
                Name:      target.Spec.RenderRegistryRef.Name,
181✔
211
                Namespace: registryNamespace,
181✔
212
        }, registry); err != nil {
227✔
213
                if apierrors.IsNotFound(err) {
92✔
214
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
46✔
215
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
46✔
216
                                return ctrl.Result{}, condErr
×
217
                        }
×
218

219
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
46✔
220
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
46✔
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 {
153✔
253
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "RegistryBindingConflict",
20✔
254
                        err.Error()); condErr != nil {
20✔
255
                        return ctrl.Result{}, condErr
×
256
                }
×
257

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

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

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

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

113✔
283
        if len(bindingList.Items) == 0 {
123✔
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
103✔
308

103✔
309
        pendingDeps := false
103✔
310

103✔
311
        for _, binding := range bindingList.Items {
232✔
312
                rel := &solarv1alpha1.Release{}
129✔
313
                if err := r.Get(ctx, client.ObjectKey{
129✔
314
                        Name:      binding.Spec.ReleaseRef.Name,
129✔
315
                        Namespace: binding.Namespace,
129✔
316
                }, rel); err != nil {
129✔
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{}
129✔
328
                cvNamespace := rel.Namespace
129✔
329
                if rel.Spec.ComponentVersionRef.Namespace != "" {
129✔
330
                        cvNamespace = rel.Spec.ComponentVersionRef.Namespace
×
331
                }
×
332

333
                if cvNamespace != rel.Namespace {
129✔
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{
129✔
354
                        Name:      rel.Spec.ComponentVersionRef.Name,
129✔
355
                        Namespace: cvNamespace,
129✔
356
                }, cv); err != nil {
129✔
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())
129✔
368
                releases = append(releases, releaseInfo{
129✔
369
                        bindingKey: binding.Namespace + "/" + binding.Name,
129✔
370
                        name:       rel.Name,
129✔
371
                        release:    rel,
129✔
372
                        cv:         cv,
129✔
373
                        rtName:     rtName,
129✔
374
                })
129✔
375
        }
376

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

384
        if len(releases) == 0 && !pendingDeps {
103✔
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
103✔
396

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

120✔
401
                switch {
120✔
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:
91✔
431
                        // RenderTask exists — check for spec drift (e.g. pull secrets
91✔
432
                        // changed after a RegistryBinding was created/updated).
91✔
433
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
91✔
434
                        if specErr != nil {
91✔
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) {
93✔
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) {
110✔
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 != "" {
156✔
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✔
NEW
485
                                if errors.Is(err, errArtifactTerminating) {
×
NEW
486
                                        log.V(1).Info("RenderArtifact is terminating; deferring RenderBinding until it is gone",
×
NEW
487
                                                "renderArtifact", aName)
×
NEW
488

×
NEW
489
                                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
NEW
490
                                }
×
491

492
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for release")
×
493
                        }
494
                        if err := r.ensureRenderArtifact(ctx, aName, rt, target.Spec.RenderRegistryRef); err != nil {
46✔
495
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for release")
×
496
                        }
×
497
                        releases[i].artifactName = aName
46✔
498
                        releases[i].artifactBindingName = bName
46✔
499
                } else {
64✔
500
                        allRendered = false
64✔
501
                }
64✔
502
        }
503

504
        if pendingDeps {
93✔
505
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
506
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
507
                        return ctrl.Result{}, condErr
×
508
                }
×
509

510
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
511
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
×
512
        }
513

514
        if !allRendered {
154✔
515
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
61✔
516
                        "Waiting for release RenderTasks to complete"); condErr != nil {
63✔
517
                        return ctrl.Result{}, condErr
2✔
518
                }
2✔
519

520
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
59✔
521
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
59✔
522
        }
523

524
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
32✔
525
                "All releases rendered successfully"); condErr != nil {
32✔
526
                return ctrl.Result{}, condErr
×
527
        }
×
528

529
        // Determine if a new bootstrap render is needed by checking whether the
530
        // current bootstrapVersion's RenderTask still matches the desired release set.
531
        bootstrapVersion := target.Status.BootstrapVersion
32✔
532
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
32✔
533
        bootstrapRT := &solarv1alpha1.RenderTask{}
32✔
534
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
32✔
535

32✔
536
        needsNewBootstrap := false
32✔
537

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

552
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
27✔
553
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
29✔
554
                        bootstrapVersion++
2✔
555
                        needsNewBootstrap = true
2✔
556
                }
2✔
557
        }
558

559
        if needsNewBootstrap {
39✔
560
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
7✔
561
                if specErr != nil {
7✔
562
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
563
                }
×
564

565
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
7✔
566
                bootstrapRT = &solarv1alpha1.RenderTask{
7✔
567
                        ObjectMeta: metav1.ObjectMeta{
7✔
568
                                Name:      bootstrapRTName,
7✔
569
                                Namespace: target.Namespace,
7✔
570
                        },
7✔
571
                        Spec: spec,
7✔
572
                }
7✔
573

7✔
574
                if err := r.Create(ctx, bootstrapRT); err != nil {
7✔
575
                        if !apierrors.IsAlreadyExists(err) {
×
576
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
577
                        }
×
578

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

588
                // Persist the new bootstrapVersion in status
589
                if bootstrapVersion != target.Status.BootstrapVersion {
9✔
590
                        target.Status.BootstrapVersion = bootstrapVersion
2✔
591
                        if err := r.Status().Update(ctx, target); err != nil {
2✔
592
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
593
                        }
×
594
                }
595
        }
596

597
        // Update target status from bootstrap RenderTask
598
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
32✔
599
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
600
                        "Bootstrap rendering failed"); condErr != nil {
×
601
                        return ctrl.Result{}, condErr
×
602
                }
×
603

604
                return ctrl.Result{}, nil
×
605
        }
606

607
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
41✔
608
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
9✔
609
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
9✔
610
                        return ctrl.Result{}, condErr
×
611
                }
×
612

613
                // Ensure RenderArtifact + RenderBinding exist for the bootstrap chart.
614
                bootstrapArtifactName := renderArtifactName(target.Namespace, bootstrapRT.Spec.BaseURL, bootstrapRT.Spec.Repository, bootstrapRT.Spec.Tag)
9✔
615
                bootstrapBindingName := renderBindingName(bootstrapArtifactName, target.Name)
9✔
616
                // Create the RenderBinding before the RenderArtifact to avoid a race
9✔
617
                if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName, target.Spec.RenderRegistryRef); err != nil {
9✔
NEW
618
                        if errors.Is(err, errArtifactTerminating) {
×
NEW
619
                                log.V(1).Info("RenderArtifact is terminating; deferring RenderBinding until it is gone",
×
NEW
620
                                        "renderArtifact", bootstrapArtifactName)
×
NEW
621

×
NEW
622
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
NEW
623
                        }
×
624

625
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for bootstrap")
×
626
                }
627
                if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, target.Spec.RenderRegistryRef); err != nil {
9✔
628
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for bootstrap")
×
629
                }
×
630

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

642
                // Clean up stale RenderBindings owned by this target.
643
                currentBindingNames := map[string]struct{}{bootstrapBindingName: {}}
9✔
644
                for _, ri := range releases {
23✔
645
                        if ri.artifactBindingName != "" {
28✔
646
                                currentBindingNames[ri.artifactBindingName] = struct{}{}
14✔
647
                        }
14✔
648
                }
649
                if err := r.deleteStaleRenderBindings(ctx, target, currentBindingNames); err != nil {
9✔
650
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
651
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
652
                        log.Error(err, "failed to clean up stale RenderBindings")
×
653
                }
×
654

655
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
9✔
656
        }
657

658
        // Still running
659
        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
23✔
660
}
661

662
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
438✔
663
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
438✔
664
                Type:               condType,
438✔
665
                Status:             status,
438✔
666
                ObservedGeneration: target.Generation,
438✔
667
                Reason:             reason,
438✔
668
                Message:            message,
438✔
669
        })
438✔
670
        if changed {
540✔
671
                if err := r.Status().Update(ctx, target); err != nil {
104✔
672
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
2✔
673
                }
2✔
674
        }
675

676
        return nil
436✔
677
}
678

679
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
103✔
680
        if len(skipped) == 0 {
197✔
681
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
94✔
682
        }
94✔
683

684
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
685
}
686

687
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
688
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
689
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
690
// It returns the accepted releases and a slice of human-readable filter messages.
691
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
112✔
692
        if len(releases) == 0 {
113✔
693
                return releases, nil
1✔
694
        }
1✔
695

696
        // Step A: uniqueName deduplication.
697
        // When UniqueName is empty, fall back to the parent Component name from the CV.
698
        namedGroups := map[string][]releaseInfo{}
111✔
699

111✔
700
        for i, ri := range releases {
254✔
701
                uname := effectiveUniqueName(ri.release, ri.cv)
143✔
702
                releases[i].uniqueName = uname
143✔
703
                namedGroups[uname] = append(namedGroups[uname], releases[i])
143✔
704
        }
143✔
705

706
        var accepted []releaseInfo
111✔
707

111✔
708
        var skipped []string
111✔
709

111✔
710
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
111✔
711
        byPriority := func(a, b releaseInfo) bool {
143✔
712
                if a.release.Spec.Priority != b.release.Spec.Priority {
42✔
713
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
714
                }
10✔
715

716
                return a.bindingKey < b.bindingKey
22✔
717
        }
718

719
        uniqueNames := make([]string, 0, len(namedGroups))
111✔
720
        for k := range namedGroups {
245✔
721
                uniqueNames = append(uniqueNames, k)
134✔
722
        }
134✔
723

724
        sort.Strings(uniqueNames)
111✔
725

111✔
726
        for _, uniqueName := range uniqueNames {
245✔
727
                group := namedGroups[uniqueName]
134✔
728
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
143✔
729

730
                accepted = append(accepted, group[0])
134✔
731

134✔
732
                for _, loser := range group[1:] {
143✔
733
                        skipped = append(skipped, fmt.Sprintf(
9✔
734
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
735
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
736
                        ))
9✔
737
                }
9✔
738
        }
739

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

745
        resolved := make([]releaseInfo, 0, len(accepted))
111✔
746

111✔
747
        for _, ri := range accepted {
245✔
748
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
134✔
749
                var riSelector labels.Selector
134✔
750
                if ri.release.Spec.AntiAffinity != nil {
141✔
751
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
752
                        if err != nil {
8✔
753
                                skipped = append(skipped, fmt.Sprintf(
1✔
754
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
755
                                        ri.bindingKey, err,
1✔
756
                                ))
1✔
757

1✔
758
                                continue
1✔
759
                        }
760

761
                        riSelector = sel
6✔
762
                }
763

764
                // Check both directions: ri's anti-affinity against already-resolved labels,
765
                // and already-resolved anti-affinities against ri's labels.
766
                conflict := ""
133✔
767
                for _, other := range resolved {
156✔
768
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
27✔
769
                                conflict = other.bindingKey
4✔
770
                                break
4✔
771
                        }
772

773
                        if other.release.Spec.AntiAffinity != nil {
20✔
774
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
775
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
776
                                        conflict = other.bindingKey
1✔
777
                                        break
1✔
778
                                }
779
                        }
780
                }
781

782
                if conflict != "" {
138✔
783
                        skipped = append(skipped, fmt.Sprintf(
5✔
784
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
785
                                ri.bindingKey, conflict,
5✔
786
                        ))
5✔
787
                } else {
133✔
788
                        resolved = append(resolved, ri)
128✔
789
                }
128✔
790
        }
791

792
        return resolved, skipped
111✔
793
}
794

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

19✔
801
        rtList := &solarv1alpha1.RenderTaskList{}
19✔
802
        if err := r.List(ctx, rtList,
19✔
803
                client.InNamespace(target.Namespace),
19✔
804
                client.MatchingFields{indexOwnerKind: "Target"},
19✔
805
        ); err != nil {
19✔
806
                return err
×
807
        }
×
808

809
        for i := range rtList.Items {
45✔
810
                rt := &rtList.Items[i]
26✔
811
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
26✔
812
                        continue
×
813
                }
814

815
                if _, current := currentRTNames[rt.Name]; current {
49✔
816
                        continue
23✔
817
                }
818

819
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
3✔
820
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
3✔
821
                        return err
×
822
                }
×
823

824
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
3✔
825
                        "Deleted stale RenderTask %s", rt.Name)
3✔
826
        }
827

828
        return nil
19✔
829
}
830

831
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
832
        rtList := &solarv1alpha1.RenderTaskList{}
2✔
833
        if err := r.List(ctx, rtList,
2✔
834
                client.InNamespace(target.Namespace),
2✔
835
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
836
        ); err != nil {
2✔
837
                return err
×
838
        }
×
839

840
        for i := range rtList.Items {
4✔
841
                rt := &rtList.Items[i]
2✔
842
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
4✔
843
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
2✔
844
                                return err
×
845
                        }
×
846
                }
847
        }
848

849
        return nil
2✔
850
}
851

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

19✔
857
        bindingList := &solarv1alpha1.RenderBindingList{}
19✔
858
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
19✔
859
                return err
×
860
        }
×
861

862
        for i := range bindingList.Items {
45✔
863
                b := &bindingList.Items[i]
26✔
864
                if b.Spec.OwnerKind != "Target" || b.Spec.OwnerName != target.Name || b.Spec.OwnerNamespace != target.Namespace {
26✔
865
                        continue
×
866
                }
867

868
                if _, current := currentBindingNames[b.Name]; current {
49✔
869
                        continue
23✔
870
                }
871

872
                log.V(1).Info("Deleting stale RenderBinding", "renderBinding", b.Name)
3✔
873
                if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
3✔
874
                        return err
×
875
                }
×
876
        }
877

878
        return nil
19✔
879
}
880

881
// deleteOwnedRenderBindings removes all RenderBindings owned by this target.
882
// Called during Target deletion to trigger GC of any associated RenderArtifacts.
883
func (r *TargetReconciler) deleteOwnedRenderBindings(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
884
        bindingList := &solarv1alpha1.RenderBindingList{}
2✔
885
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
2✔
886
                return err
×
887
        }
×
888

889
        for i := range bindingList.Items {
3✔
890
                b := &bindingList.Items[i]
1✔
891
                if b.Spec.OwnerKind == "Target" && b.Spec.OwnerName == target.Name && b.Spec.OwnerNamespace == target.Namespace {
2✔
892
                        if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
1✔
893
                                return err
×
894
                        }
×
895
                }
896
        }
897

898
        return nil
2✔
899
}
900

901
// ensureRenderArtifact creates a RenderArtifact for the given RenderTask's OCI coordinates
902
// if one does not already exist. Idempotent: if it already exists (possibly created by
903
// another Target reconciling the same shared artifact), this is a no-op
904
// RegistryRef for an existing artifact is kept in sync separately, by RenderArtifactReconciler
905
// re-pinning from RenderBinding snapshots (see ensureRenderBinding below).
906
func (r *TargetReconciler) ensureRenderArtifact(ctx context.Context, name string, rt *solarv1alpha1.RenderTask, registryRef solarv1alpha1.ObjectReference) error {
58✔
907
        log := ctrl.LoggerFrom(ctx)
58✔
908

58✔
909
        artifact := &solarv1alpha1.RenderArtifact{}
58✔
910
        key := client.ObjectKey{Name: name, Namespace: rt.Namespace}
58✔
911
        if err := r.Get(ctx, key, artifact); err == nil {
104✔
912
                if !artifact.DeletionTimestamp.IsZero() {
47✔
913
                        // The artifact is terminating (stuck in OCI cleanup). The RenderBinding pointing
1✔
914
                        // at it has already been ensured, and a fresh artifact is created on a later
1✔
915
                        // reconcile once deletion completes. Ignore it so a stuck finalizer cannot block
1✔
916
                        // this Target from reconciling successfully.
1✔
917
                        log.V(1).Info("RenderArtifact is terminating; ignoring", "renderArtifact", key)
1✔
918

1✔
919
                        return nil
1✔
920
                }
1✔
921

922
                return nil
45✔
923
        } else if !apierrors.IsNotFound(err) {
12✔
924
                return err
×
925
        }
×
926

927
        artifact = &solarv1alpha1.RenderArtifact{
12✔
928
                ObjectMeta: metav1.ObjectMeta{
12✔
929
                        Name:      name,
12✔
930
                        Namespace: rt.Namespace,
12✔
931
                },
12✔
932
                Spec: solarv1alpha1.RenderArtifactSpec{
12✔
933
                        BaseURL:       rt.Spec.BaseURL,
12✔
934
                        Repository:    rt.Spec.Repository,
12✔
935
                        Tag:           rt.Spec.Tag,
12✔
936
                        RenderTaskRef: rt.Name,
12✔
937
                        RegistryRef:   &registryRef,
12✔
938
                },
12✔
939
        }
12✔
940

12✔
941
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
12✔
942
                return err
×
943
        }
×
944

945
        return nil
12✔
946
}
947

948
// errArtifactTerminating is returned by ensureRenderBinding when the RenderArtifact a
949
// Target wants to bind to is being deleted. Callers translate it into a clean requeue so no
950
// new RenderBinding can reference a terminating RenderArtifact, which would otherwise race
951
// with RenderArtifactReconciler's OCI tag cleanup (a binding created in the window between
952
// its bound-check and the tag delete could lose its tag).
953
var errArtifactTerminating = errors.New("renderartifact is terminating")
954

955
// ensureRenderBinding creates a RenderBinding linking this Target to the named
956
// RenderArtifact if one does not already exist, and keeps an existing binding's Registry
957
// snapshot in sync with the Target's current reference. Idempotent.
958
//
959
// The binding snapshots the Registry this Target currently resolves (registryRef) so
960
// RenderArtifactReconciler can re-pin the shared RenderArtifact's RegistryRef from a
961
// surviving binding whenever another binding referencing the same artifact is removed.
962
// A snapshot left behind after Target.Spec.RenderRegistryRef changes outlives the Registry
963
// it names: once that Registry or its Secret is gone, re-pinning hands the artifact a
964
// reference cleanup cannot resolve and the finalizer stays stuck. Patching here re-triggers
965
// RenderArtifactReconciler (it watches RenderBindings), which re-pins the artifact.
966
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string, registryRef solarv1alpha1.ObjectReference) error {
59✔
967
        binding := &solarv1alpha1.RenderBinding{}
59✔
968
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
104✔
969
                if registryRefEqual(binding.Spec.RegistryRef, &registryRef) {
89✔
970
                        return nil
44✔
971
                }
44✔
972

973
                latest := binding.DeepCopy()
1✔
974
                latest.Spec.RegistryRef = &registryRef
1✔
975

1✔
976
                return r.Patch(ctx, latest, client.MergeFrom(binding))
1✔
977
        } else if !apierrors.IsNotFound(err) {
14✔
978
                return err
×
979
        }
×
980

981
        // Refuse to create a binding that references a terminating RenderArtifact. The deletion
982
        // path only removes the OCI tag while no binding exists; creating one in that window
983
        // could lose the tag. The artifact normally does not exist yet (the binding is created
984
        // first), so this only fires when a shared artifact is mid-deletion.
985
        artifact := &solarv1alpha1.RenderArtifact{}
14✔
986
        if err := r.Get(ctx, client.ObjectKey{Name: artifactName, Namespace: target.Namespace}, artifact); err == nil {
16✔
987
                if !artifact.DeletionTimestamp.IsZero() {
3✔
988
                        return errArtifactTerminating
1✔
989
                }
1✔
990
        } else if !apierrors.IsNotFound(err) {
12✔
NEW
991
                return err
×
NEW
992
        }
×
993

994
        binding = &solarv1alpha1.RenderBinding{
13✔
995
                ObjectMeta: metav1.ObjectMeta{
13✔
996
                        Name:      bindingName,
13✔
997
                        Namespace: target.Namespace,
13✔
998
                },
13✔
999
                Spec: solarv1alpha1.RenderBindingSpec{
13✔
1000
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
13✔
1001
                        OwnerKind:         "Target",
13✔
1002
                        OwnerName:         target.Name,
13✔
1003
                        OwnerNamespace:    target.Namespace,
13✔
1004
                        RegistryRef:       &registryRef,
13✔
1005
                },
13✔
1006
        }
13✔
1007

13✔
1008
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
13✔
1009
                return err
×
1010
        }
×
1011

1012
        return nil
13✔
1013
}
1014

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

120✔
1019
        var targetNamespace string
120✔
1020
        if rel.Spec.TargetNamespace != nil {
240✔
1021
                targetNamespace = *rel.Spec.TargetNamespace
120✔
1022
        }
120✔
1023

1024
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
120✔
1025
        if err != nil {
130✔
1026
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
1027
        }
10✔
1028

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

110✔
1035
        return solarv1alpha1.RenderTaskSpec{
110✔
1036
                RendererConfig: solarv1alpha1.RendererConfig{
110✔
1037
                        Type: solarv1alpha1.RendererConfigTypeRelease,
110✔
1038
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
110✔
1039
                                Chart: solarv1alpha1.ChartConfig{
110✔
1040
                                        Name:        chartName,
110✔
1041
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
110✔
1042
                                        Version:     tag,
110✔
1043
                                        AppVersion:  tag,
110✔
1044
                                },
110✔
1045
                                Input: solarv1alpha1.ReleaseInput{
110✔
1046
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
110✔
1047
                                        Resources:  resolvedResources,
110✔
1048
                                        Entrypoint: cv.Spec.Entrypoint,
110✔
1049
                                },
110✔
1050
                                Values:          rel.Spec.Values,
110✔
1051
                                TargetNamespace: targetNamespace,
110✔
1052
                        },
110✔
1053
                },
110✔
1054
                Repository:     repo,
110✔
1055
                Tag:            tag,
110✔
1056
                BaseURL:        registry.Spec.Hostname,
110✔
1057
                PlainHTTP:      registry.Spec.PlainHTTP,
110✔
1058
                PushSecretRef:  registry.Spec.SolarSecretRef,
110✔
1059
                FailedJobTTL:   rel.Spec.FailedJobTTL,
110✔
1060
                OwnerName:      target.Name,
110✔
1061
                OwnerNamespace: target.Namespace,
110✔
1062
                OwnerKind:      "Target",
110✔
1063
        }, nil
110✔
1064
}
1065

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

39✔
1071
        for _, ri := range releases {
91✔
1072
                if ri.uniqueName == "" {
53✔
1073
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
1074
                }
1✔
1075

1076
                trimmedRef := strings.TrimPrefix(ri.chartURL, "oci://")
51✔
1077
                ref, err := ociname.ParseReference(trimmedRef)
51✔
1078
                if err != nil {
51✔
1079
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
1080
                }
×
1081

1082
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
51✔
1083
                        Repository:     ref.Context().String(),
51✔
1084
                        Tag:            ref.Identifier(),
51✔
1085
                        PullSecretName: renderRegistryPullSecret,
51✔
1086
                        Insecure:       insecure,
51✔
1087
                }
51✔
1088
        }
1089

1090
        return solarv1alpha1.BootstrapInput{
38✔
1091
                Releases: resolvedReleases,
38✔
1092
                Userdata: target.Spec.Userdata,
38✔
1093
        }, nil
38✔
1094
}
1095

1096
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
7✔
1097
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName, registry.Spec.PlainHTTP)
7✔
1098
        if err != nil {
7✔
1099
                return solarv1alpha1.RenderTaskSpec{}, err
×
1100
        }
×
1101

1102
        releaseNames := make([]string, 0, len(releases))
7✔
1103
        for _, ri := range releases {
16✔
1104
                releaseNames = append(releaseNames, ri.name)
9✔
1105
        }
9✔
1106

1107
        sort.Strings(releaseNames)
7✔
1108

7✔
1109
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
7✔
1110
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
1111
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
7✔
1112

7✔
1113
        return solarv1alpha1.RenderTaskSpec{
7✔
1114
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
1115
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
7✔
1116
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
7✔
1117
                                Chart: solarv1alpha1.ChartConfig{
7✔
1118
                                        Name:        chartName,
7✔
1119
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
7✔
1120
                                        Version:     tag,
7✔
1121
                                        AppVersion:  tag,
7✔
1122
                                },
7✔
1123
                                Input: input,
7✔
1124
                        },
7✔
1125
                },
7✔
1126
                Repository:     repo,
7✔
1127
                Tag:            tag,
7✔
1128
                BaseURL:        registry.Spec.Hostname,
7✔
1129
                PlainHTTP:      registry.Spec.PlainHTTP,
7✔
1130
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
1131
                OwnerName:      target.Name,
7✔
1132
                OwnerNamespace: target.Namespace,
7✔
1133
                OwnerKind:      "Target",
7✔
1134
        }, nil
7✔
1135
}
1136

1137
// SetupWithManager sets up the controller with the Manager.
1138
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
1139
        return ctrl.NewControllerManagedBy(mgr).
1✔
1140
                For(&solarv1alpha1.Target{}).
1✔
1141
                Watches(
1✔
1142
                        &solarv1alpha1.ReleaseBinding{},
1✔
1143
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
1144
                ).
1✔
1145
                Watches(
1✔
1146
                        &solarv1alpha1.RenderTask{},
1✔
1147
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
1148
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
1149
                ).
1✔
1150
                Watches(
1✔
1151
                        &solarv1alpha1.Registry{},
1✔
1152
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
1153
                ).
1✔
1154
                Watches(
1✔
1155
                        &solarv1alpha1.RegistryBinding{},
1✔
1156
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
1157
                ).
1✔
1158
                Watches(
1✔
1159
                        &solarv1alpha1.ReferenceGrant{},
1✔
1160
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
1161
                ).
1✔
1162
                Watches(
1✔
1163
                        &solarv1alpha1.Release{},
1✔
1164
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
1165
                ).
1✔
1166
                Complete(r)
1✔
1167
}
1✔
1168

1169
// registryGranted checks whether a ReferenceGrant in registryNamespace permits a
1170
// resource of fromKind in fromNamespace to reference the named registry.
1171
// fromKind is the kind that is actually resolving the reference: each kind needs its
1172
// own grant subject, so a grant written for one kind never authorizes another.
1173
func registryGranted(ctx context.Context, reader client.Reader, registryNamespace, fromKind, fromNamespace string) (bool, error) {
11✔
1174
        grantList := &solarv1alpha1.ReferenceGrantList{}
11✔
1175
        if err := reader.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
12✔
1176
                return false, err
1✔
1177
        }
1✔
1178
        for i := range grantList.Items {
16✔
1179
                grant := &grantList.Items[i]
6✔
1180
                if grantPermitsRegistryAccess(grant, fromKind, fromNamespace) {
10✔
1181
                        return true, nil
4✔
1182
                }
4✔
1183
        }
1184

1185
        return false, nil
6✔
1186
}
1187

1188
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a resource of
1189
// fromKind in fromNamespace to reference Registry resources in the grant's namespace.
1190
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromKind, fromNamespace string) bool {
10✔
1191
        return grantPermits(grant, solarGroup, fromKind, fromNamespace, solarGroup, "Registry")
10✔
1192
}
10✔
1193

1194
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1195
// Targets that reference it — either in the same namespace or cross-namespace.
1196
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
235✔
1197
        reg, ok := obj.(*solarv1alpha1.Registry)
235✔
1198
        if !ok {
235✔
1199
                return nil
×
1200
        }
×
1201

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

×
1207
                return nil
×
1208
        }
×
1209

1210
        var requests []reconcile.Request
235✔
1211
        for _, t := range targetList.Items {
305✔
1212
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
70✔
1213
                        (t.Spec.RenderRegistryRef.Namespace == "" || t.Spec.RenderRegistryRef.Namespace == reg.Namespace) {
126✔
1214
                        requests = append(requests, reconcile.Request{
56✔
1215
                                NamespacedName: types.NamespacedName{
56✔
1216
                                        Name:      t.Name,
56✔
1217
                                        Namespace: t.Namespace,
56✔
1218
                                },
56✔
1219
                        })
56✔
1220
                }
56✔
1221
        }
1222

1223
        // Cross-namespace targets: find namespaces that have been granted access to
1224
        // registries in reg.Namespace, then check their targets.
1225
        grantList := &solarv1alpha1.ReferenceGrantList{}
235✔
1226
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
235✔
1227
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
1228
                return requests
×
1229
        }
×
1230

1231
        for i := range grantList.Items {
258✔
1232
                grant := &grantList.Items[i]
23✔
1233
                if !grantsRegistryResource(grant) {
43✔
1234
                        continue
20✔
1235
                }
1236
                for _, from := range grant.Spec.From {
6✔
1237
                        if from.Kind != "Target" || from.Group != solarGroup {
3✔
1238
                                continue
×
1239
                        }
1240
                        crossTargets := &solarv1alpha1.TargetList{}
3✔
1241
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
3✔
1242
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
1243
                                continue
×
1244
                        }
1245
                        for _, t := range crossTargets.Items {
6✔
1246
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryRef.Namespace == reg.Namespace {
6✔
1247
                                        requests = append(requests, reconcile.Request{
3✔
1248
                                                NamespacedName: types.NamespacedName{
3✔
1249
                                                        Name:      t.Name,
3✔
1250
                                                        Namespace: t.Namespace,
3✔
1251
                                                },
3✔
1252
                                        })
3✔
1253
                                }
3✔
1254
                        }
1255
                }
1256
        }
1257

1258
        return requests
235✔
1259
}
1260

1261
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
1262
// each bound Registry, and returns a map from registry hostname to
1263
// targetPullSecretName. Registries without a targetPullSecretName are included
1264
// with an empty string (anonymous pull).
1265
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
133✔
1266
        rbList := &solarv1alpha1.RegistryBindingList{}
133✔
1267
        if err := r.List(ctx, rbList,
133✔
1268
                client.InNamespace(target.Namespace),
133✔
1269
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
133✔
1270
        ); err != nil {
133✔
1271
                return nil, err
×
1272
        }
×
1273

1274
        type hostEntry struct {
133✔
1275
                pullSecret  string
133✔
1276
                bindingName string
133✔
1277
        }
133✔
1278

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

133✔
1281
        for _, rb := range rbList.Items {
173✔
1282
                reg := &solarv1alpha1.Registry{}
40✔
1283
                if err := r.Get(ctx, client.ObjectKey{
40✔
1284
                        Name:      rb.Spec.RegistryRef.Name,
40✔
1285
                        Namespace: rb.Namespace,
40✔
1286
                }, reg); err != nil {
50✔
1287
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1288
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1289
                }
10✔
1290

1291
                host := strings.ToLower(reg.Spec.Hostname)
30✔
1292
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
40✔
1293
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
10✔
1294
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1295
                }
10✔
1296

1297
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
20✔
1298
        }
1299

1300
        result := make(map[string]string, len(lookup))
113✔
1301
        for host, entry := range lookup {
123✔
1302
                result[host] = entry.pullSecret
10✔
1303
        }
10✔
1304

1305
        return result, nil
113✔
1306
}
1307

1308
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1309
// for the referenced Target.
1310
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
66✔
1311
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
66✔
1312
        if !ok {
66✔
1313
                return nil
×
1314
        }
×
1315

1316
        if rb.Spec.TargetRef.Name == "" {
66✔
1317
                return nil
×
1318
        }
×
1319

1320
        return []reconcile.Request{
66✔
1321
                {
66✔
1322
                        NamespacedName: types.NamespacedName{
66✔
1323
                                Name:      rb.Spec.TargetRef.Name,
66✔
1324
                                Namespace: rb.Namespace,
66✔
1325
                        },
66✔
1326
                },
66✔
1327
        }
66✔
1328
}
1329

1330
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1331
// either because the grant controls Registry access (Target → Registry) or because
1332
// it controls ComponentVersion access (Release → ComponentVersion).
1333
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
18✔
1334
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
18✔
1335
        if !ok {
18✔
1336
                return nil
×
1337
        }
×
1338

1339
        var requests []reconcile.Request
18✔
1340

18✔
1341
        if grantsRegistryResource(grant) {
23✔
1342
                for _, from := range grant.Spec.From {
10✔
1343
                        if from.Kind != "Target" || from.Group != solarGroup {
6✔
1344
                                continue
1✔
1345
                        }
1346
                        targets := &solarv1alpha1.TargetList{}
4✔
1347
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
1348
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1349
                                continue
×
1350
                        }
1351
                        for _, t := range targets.Items {
6✔
1352
                                // Enqueue targets that reference a registry specifically in the grant's namespace
2✔
1353
                                if t.Spec.RenderRegistryRef.Namespace == grant.Namespace {
4✔
1354
                                        requests = append(requests, reconcile.Request{
2✔
1355
                                                NamespacedName: types.NamespacedName{
2✔
1356
                                                        Name:      t.Name,
2✔
1357
                                                        Namespace: t.Namespace,
2✔
1358
                                                },
2✔
1359
                                        })
2✔
1360
                                }
2✔
1361
                        }
1362
                }
1363
        }
1364

1365
        if grantsComponentVersionResource(grant) {
23✔
1366
                seen := map[string]struct{}{}
5✔
1367
                for _, from := range grant.Spec.From {
10✔
1368
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
1369
                                continue
×
1370
                        }
1371
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
1372
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
1373
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1374
                                continue
×
1375
                        }
1376
                        for _, rb := range bindings.Items {
6✔
1377
                                if rb.Spec.TargetRef.Name == "" {
1✔
1378
                                        continue
×
1379
                                }
1380
                                targetNs := rb.Namespace
1✔
1381
                                if rb.Spec.TargetRef.Namespace != "" {
2✔
1382
                                        targetNs = rb.Spec.TargetRef.Namespace
1✔
1383
                                }
1✔
1384
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1385
                                if _, ok := seen[key]; ok {
1✔
1386
                                        continue
×
1387
                                }
1388
                                seen[key] = struct{}{}
1✔
1389
                                requests = append(requests, reconcile.Request{
1✔
1390
                                        NamespacedName: types.NamespacedName{
1✔
1391
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1392
                                                Namespace: targetNs,
1✔
1393
                                        },
1✔
1394
                                })
1✔
1395
                        }
1396
                }
1397
        }
1398

1399
        if grantsReleaseBindingToTargetResource(grant) {
22✔
1400
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1401
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1402
                // the new or removed cross-namespace ReleaseBindings.
4✔
1403
                targets := &solarv1alpha1.TargetList{}
4✔
1404
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1405
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1406
                } else {
4✔
1407
                        for _, t := range targets.Items {
8✔
1408
                                requests = append(requests, reconcile.Request{
4✔
1409
                                        NamespacedName: types.NamespacedName{
4✔
1410
                                                Name:      t.Name,
4✔
1411
                                                Namespace: t.Namespace,
4✔
1412
                                        },
4✔
1413
                                })
4✔
1414
                        }
4✔
1415
                }
1416
        }
1417

1418
        return requests
18✔
1419
}
1420

1421
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1422
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
43✔
1423
        for _, t := range grant.Spec.To {
86✔
1424
                if t.Kind == "Registry" && t.Group == solarGroup {
52✔
1425
                        return true
9✔
1426
                }
9✔
1427
        }
1428

1429
        return false
34✔
1430
}
1431

1432
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1433
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1434
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
31✔
1435
        hasReleaseBindingFrom := false
31✔
1436
        for _, f := range grant.Spec.From {
62✔
1437
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
48✔
1438
                        hasReleaseBindingFrom = true
17✔
1439
                        break
17✔
1440
                }
1441
        }
1442
        if !hasReleaseBindingFrom {
45✔
1443
                return false
14✔
1444
        }
14✔
1445
        for _, t := range grant.Spec.To {
34✔
1446
                if t.Kind == "Target" && t.Group == solarGroup {
34✔
1447
                        return true
17✔
1448
                }
17✔
1449
        }
1450

1451
        return false
×
1452
}
1453

1454
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1455
// that reference target via spec.targetRef.name + spec.targetRef.namespace, authorized by
1456
// a ReferenceGrant in target's namespace.
1457
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
113✔
1458
        grantList := &solarv1alpha1.ReferenceGrantList{}
113✔
1459
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
113✔
1460
                return nil, err
×
1461
        }
×
1462

1463
        seen := make(map[string]struct{})
113✔
1464
        var result []solarv1alpha1.ReleaseBinding
113✔
1465
        for i := range grantList.Items {
126✔
1466
                grant := &grantList.Items[i]
13✔
1467
                if !grantsReleaseBindingToTargetResource(grant) {
13✔
1468
                        continue
×
1469
                }
1470
                for _, from := range grant.Spec.From {
26✔
1471
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
13✔
1472
                                continue
×
1473
                        }
1474
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
13✔
1475
                        if err := r.List(ctx, crossBindings,
13✔
1476
                                client.InNamespace(from.Namespace),
13✔
1477
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
13✔
1478
                        ); err != nil {
13✔
1479
                                return nil, err
×
1480
                        }
×
1481
                        for _, rb := range crossBindings.Items {
26✔
1482
                                if rb.Spec.TargetRef.Namespace != target.Namespace {
13✔
1483
                                        continue
×
1484
                                }
1485
                                key := rb.Namespace + "/" + rb.Name
13✔
1486
                                if _, exists := seen[key]; exists {
16✔
1487
                                        continue
3✔
1488
                                }
1489
                                seen[key] = struct{}{}
10✔
1490
                                result = append(result, rb)
10✔
1491
                        }
1492
                }
1493
        }
1494

1495
        return result, nil
113✔
1496
}
1497

1498
// mapReleaseToTargets maps a Release event to reconcile requests for all
1499
// Targets that are bound to the release via ReleaseBindings.
1500
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
428✔
1501
        rel, ok := obj.(*solarv1alpha1.Release)
428✔
1502
        if !ok {
428✔
1503
                return nil
×
1504
        }
×
1505

1506
        bindingList := &solarv1alpha1.ReleaseBindingList{}
428✔
1507
        if err := r.List(ctx, bindingList,
428✔
1508
                client.InNamespace(rel.Namespace),
428✔
1509
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
428✔
1510
        ); err != nil {
428✔
1511
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1512

×
1513
                return nil
×
1514
        }
×
1515

1516
        seen := map[string]struct{}{}
428✔
1517
        var requests []reconcile.Request
428✔
1518

428✔
1519
        for _, rb := range bindingList.Items {
628✔
1520
                targetNs := rb.Namespace
200✔
1521
                if rb.Spec.TargetRef.Namespace != "" {
200✔
1522
                        targetNs = rb.Spec.TargetRef.Namespace
×
1523
                }
×
1524

1525
                key := targetNs + "/" + rb.Spec.TargetRef.Name
200✔
1526
                if _, ok := seen[key]; ok {
204✔
1527
                        continue
4✔
1528
                }
1529

1530
                seen[key] = struct{}{}
196✔
1531
                requests = append(requests, reconcile.Request{
196✔
1532
                        NamespacedName: types.NamespacedName{
196✔
1533
                                Name:      rb.Spec.TargetRef.Name,
196✔
1534
                                Namespace: targetNs,
196✔
1535
                        },
196✔
1536
                })
196✔
1537
        }
1538

1539
        return requests
428✔
1540
}
1541

1542
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
235✔
1543
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
235✔
1544
        if !ok || rb.Spec.TargetRef.Name == "" {
235✔
1545
                return nil
×
1546
        }
×
1547

1548
        targetNs := rb.Namespace
235✔
1549
        if rb.Spec.TargetRef.Namespace != "" {
252✔
1550
                targetNs = rb.Spec.TargetRef.Namespace
17✔
1551
        }
17✔
1552

1553
        return []reconcile.Request{
235✔
1554
                {
235✔
1555
                        NamespacedName: types.NamespacedName{
235✔
1556
                                Name:      rb.Spec.TargetRef.Name,
235✔
1557
                                Namespace: targetNs,
235✔
1558
                        },
235✔
1559
                },
235✔
1560
        }
235✔
1561
}
1562

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