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

opendefensecloud / solution-arsenal / 30981137536

05 Aug 2026 06:22AM UTC coverage: 82.05% (-0.008%) from 82.058%
30981137536

push

github

web-flow
chore: update flake and remove kind version workaround (#732)

## What
Update flake and remove kind version workaround

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


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

## Summary by CodeRabbit

* **Chores**
* Removed the temporary Kind version validation from local image
loading.
* Local image loading no longer checks for a minimum Kind version or
stops early with a version-related error.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

5307 of 6468 relevant lines covered (82.05%)

33.86 hits per line

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

78.33
/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
        defaultRequeueTime = 30 * time.Second
43
)
44

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

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

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

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

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

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

495✔
103
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
719✔
104
                return ctrl.Result{}, nil
224✔
105
        }
224✔
106

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

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

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

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

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

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

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

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

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

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

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

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

183
                return ctrl.Result{}, nil
37✔
184
        }
185

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

305
                return ctrl.Result{}, nil
10✔
306
        }
307

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

102✔
311
        pendingDeps := false
102✔
312

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

×
323
                                continue
×
324
                        }
325

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

329
                cv := &solarv1alpha1.ComponentVersion{}
129✔
330
                cvNamespace := rel.Namespace
129✔
331
                if rel.Spec.ComponentVersionRef.Namespace != "" {
129✔
332
                        cvNamespace = rel.Spec.ComponentVersionRef.Namespace
×
333
                }
×
334

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

×
351
                                continue
×
352
                        }
353
                }
354

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

×
363
                                continue
×
364
                        }
365

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

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

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

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

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

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

102✔
399
        for i, ri := range releases {
222✔
400
                rt := &solarv1alpha1.RenderTask{}
120✔
401
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
120✔
402

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

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

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

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

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

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

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

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

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

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

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

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

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

47✔
481
                        // Ensure a RenderArtifact object exists for the pushed OCI artifact, and
47✔
482
                        // create a RenderBinding linking this Target to it.
47✔
483
                        aName := renderArtifactName(target.Namespace, rt.Spec.BaseURL, rt.Spec.Repository, rt.Spec.Tag)
47✔
484
                        bName := renderBindingName(aName, target.Name)
47✔
485
                        // Create the RenderBinding before the RenderArtifact to avoid a race
47✔
486
                        if err := r.ensureRenderBinding(ctx, target, aName, bName, target.Spec.RenderRegistryRef); err != nil {
47✔
487
                                if errors.Is(err, errArtifactTerminating) {
×
488
                                        log.V(1).Info("RenderArtifact is terminating; deferring RenderBinding until it is gone",
×
489
                                                "renderArtifact", aName)
×
490

×
491
                                        return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil
×
492
                                }
×
493

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

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

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

516
        if !allRendered {
152✔
517
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
60✔
518
                        "Waiting for release RenderTasks to complete"); condErr != nil {
62✔
519
                        return ctrl.Result{}, condErr
2✔
520
                }
2✔
521

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

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

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

32✔
538
        needsNewBootstrap := false
32✔
539

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

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

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

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

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

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

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

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

606
                return ctrl.Result{}, nil
×
607
        }
608

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

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

×
624
                                return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil
×
625
                        }
×
626

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

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

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

657
                return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil
9✔
658
        }
659

660
        // Still running
661
        return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil
23✔
662
}
663

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

678
        return nil
433✔
679
}
680

681
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
102✔
682
        if len(skipped) == 0 {
195✔
683
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
93✔
684
        }
93✔
685

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

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

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

110✔
702
        for i, ri := range releases {
253✔
703
                uname := effectiveUniqueName(ri.release, ri.cv)
143✔
704
                releases[i].uniqueName = uname
143✔
705
                namedGroups[uname] = append(namedGroups[uname], releases[i])
143✔
706
        }
143✔
707

708
        var accepted []releaseInfo
110✔
709

110✔
710
        var skipped []string
110✔
711

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

718
                return a.bindingKey < b.bindingKey
23✔
719
        }
720

721
        uniqueNames := make([]string, 0, len(namedGroups))
110✔
722
        for k := range namedGroups {
244✔
723
                uniqueNames = append(uniqueNames, k)
134✔
724
        }
134✔
725

726
        sort.Strings(uniqueNames)
110✔
727

110✔
728
        for _, uniqueName := range uniqueNames {
244✔
729
                group := namedGroups[uniqueName]
134✔
730
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
143✔
731

732
                accepted = append(accepted, group[0])
134✔
733

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

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

747
        resolved := make([]releaseInfo, 0, len(accepted))
110✔
748

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

1✔
760
                                continue
1✔
761
                        }
762

763
                        riSelector = sel
6✔
764
                }
765

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

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

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

794
        return resolved, skipped
110✔
795
}
796

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

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

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

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

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

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

830
        return nil
19✔
831
}
832

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

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

851
        return nil
2✔
852
}
853

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

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

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

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

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

880
        return nil
19✔
881
}
882

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

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

900
        return nil
2✔
901
}
902

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

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

1✔
921
                        return nil
1✔
922
                }
1✔
923

924
                return nil
46✔
925
        } else if !apierrors.IsNotFound(err) {
12✔
926
                return err
×
927
        }
×
928

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

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

947
        return nil
12✔
948
}
949

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

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

975
                latest := binding.DeepCopy()
1✔
976
                latest.Spec.RegistryRef = &registryRef
1✔
977

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

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

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

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

1014
        return nil
13✔
1015
}
1016

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

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

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

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

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

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

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

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

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

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

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

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

1109
        sort.Strings(releaseNames)
7✔
1110

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

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

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

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

1187
        return false, nil
6✔
1188
}
1189

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

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

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

×
1209
                return nil
×
1210
        }
×
1211

1212
        var requests []reconcile.Request
235✔
1213
        for _, t := range targetList.Items {
306✔
1214
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
71✔
1215
                        (t.Spec.RenderRegistryRef.Namespace == "" || t.Spec.RenderRegistryRef.Namespace == reg.Namespace) {
128✔
1216
                        requests = append(requests, reconcile.Request{
57✔
1217
                                NamespacedName: types.NamespacedName{
57✔
1218
                                        Name:      t.Name,
57✔
1219
                                        Namespace: t.Namespace,
57✔
1220
                                },
57✔
1221
                        })
57✔
1222
                }
57✔
1223
        }
1224

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

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

1260
        return requests
235✔
1261
}
1262

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

1276
        type hostEntry struct {
132✔
1277
                pullSecret  string
132✔
1278
                bindingName string
132✔
1279
        }
132✔
1280

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

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

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

1299
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
19✔
1300
        }
1301

1302
        result := make(map[string]string, len(lookup))
112✔
1303
        for host, entry := range lookup {
121✔
1304
                result[host] = entry.pullSecret
9✔
1305
        }
9✔
1306

1307
        return result, nil
112✔
1308
}
1309

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

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

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

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

1341
        var requests []reconcile.Request
18✔
1342

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

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

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

1420
        return requests
18✔
1421
}
1422

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

1431
        return false
35✔
1432
}
1433

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

1453
        return false
×
1454
}
1455

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

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

1497
        return result, nil
112✔
1498
}
1499

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

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

×
1515
                return nil
×
1516
        }
×
1517

1518
        seen := map[string]struct{}{}
424✔
1519
        var requests []reconcile.Request
424✔
1520

424✔
1521
        for _, rb := range bindingList.Items {
625✔
1522
                targetNs := rb.Namespace
201✔
1523
                if rb.Spec.TargetRef.Namespace != "" {
202✔
1524
                        targetNs = rb.Spec.TargetRef.Namespace
1✔
1525
                }
1✔
1526

1527
                key := targetNs + "/" + rb.Spec.TargetRef.Name
201✔
1528
                if _, ok := seen[key]; ok {
205✔
1529
                        continue
4✔
1530
                }
1531

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

1541
        return requests
424✔
1542
}
1543

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

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

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

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