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

opendefensecloud / solution-arsenal / 30349610298

28 Jul 2026 10:09AM UTC coverage: 81.857% (-0.7%) from 82.538%
30349610298

Pull #717

github

web-flow
Merge 7da18dd81 into f6f9b51cd
Pull Request #717: docs: update docs and streamline new user exp

3 of 39 new or added lines in 1 file covered. (7.69%)

14 existing lines in 3 files now uncovered.

5193 of 6344 relevant lines covered (81.86%)

29.4 hits per line

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

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

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

497✔
101
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
720✔
102
                return ctrl.Result{}, nil
223✔
103
        }
223✔
104

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

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

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

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

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

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

219
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
47✔
220
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
47✔
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) {
158✔
229
                latest := registry.DeepCopy()
26✔
230
                latest.Finalizers = append(latest.Finalizers, registryRefFinalizer)
26✔
231
                if err := r.Patch(ctx, latest, client.MergeFromWithOptions(registry, client.MergeFromWithOptimisticLock{})); err != nil {
26✔
232
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add protection finalizer to Registry")
×
233
                }
×
234
        }
235

236
        if registry.Spec.SolarSecretRef == nil {
134✔
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",
130✔
246
                "Registry resolved: "+registry.Name); condErr != nil {
130✔
247
                return ctrl.Result{}, condErr
×
248
        }
×
249

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

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

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

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

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

109✔
283
        if len(bindingList.Items) == 0 {
119✔
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
99✔
308

99✔
309
        pendingDeps := false
99✔
310

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

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

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

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

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

116✔
401
                switch {
116✔
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:
87✔
431
                        // RenderTask exists — check for spec drift (e.g. pull secrets
87✔
432
                        // changed after a RegistryBinding was created/updated).
87✔
433
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
87✔
434
                        if specErr != nil {
87✔
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) {
89✔
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) {
106✔
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 != "" {
150✔
477
                        releases[i].chartURL = rt.Status.ChartURL
44✔
478

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

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

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

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

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

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

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

30✔
529
        needsNewBootstrap := false
30✔
530

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

662
        return nil
429✔
663
}
664

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

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

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

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

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

692
        var accepted []releaseInfo
107✔
693

107✔
694
        var skipped []string
107✔
695

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

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

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

710
        sort.Strings(uniqueNames)
107✔
711

107✔
712
        for _, uniqueName := range uniqueNames {
237✔
713
                group := namedGroups[uniqueName]
130✔
714
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
139✔
715

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

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

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

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

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

1✔
744
                                continue
1✔
745
                        }
746

747
                        riSelector = sel
6✔
748
                }
749

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

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

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

778
        return resolved, skipped
107✔
779
}
780

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

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

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

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

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

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

814
        return nil
19✔
815
}
816

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

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

835
        return nil
2✔
836
}
837

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

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

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

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

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

864
        return nil
19✔
865
}
866

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

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

884
        return nil
2✔
885
}
886

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

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

908
        var pushSecretRef *solarv1alpha1.ObjectReference
11✔
909
        if rt.Spec.PushSecretRef != nil {
22✔
910
                pushSecretRef = &solarv1alpha1.ObjectReference{
11✔
911
                        Name:      rt.Spec.PushSecretRef.Name,
11✔
912
                        Namespace: pushSecretNamespace,
11✔
913
                }
11✔
914
        }
11✔
915

916
        artifact = &solarv1alpha1.RenderArtifact{
11✔
917
                ObjectMeta: metav1.ObjectMeta{
11✔
918
                        Name:      name,
11✔
919
                        Namespace: rt.Namespace,
11✔
920
                },
11✔
921
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
922
                        BaseURL:        rt.Spec.BaseURL,
11✔
923
                        Repository:     rt.Spec.Repository,
11✔
924
                        Tag:            rt.Spec.Tag,
11✔
925
                        RenderTaskRef:  rt.Name,
11✔
926
                        PushSecretRef:  pushSecretRef,
11✔
927
                        RegistryFlavor: flavor,
11✔
928
                        PlainHTTP:      rt.Spec.PlainHTTP,
11✔
929
                },
11✔
930
        }
11✔
931

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

936
        return nil
11✔
937
}
938

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

949
        binding = &solarv1alpha1.RenderBinding{
11✔
950
                ObjectMeta: metav1.ObjectMeta{
11✔
951
                        Name:      bindingName,
11✔
952
                        Namespace: target.Namespace,
11✔
953
                },
11✔
954
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
955
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
956
                        OwnerKind:         "Target",
11✔
957
                        OwnerName:         target.Name,
11✔
958
                        OwnerNamespace:    target.Namespace,
11✔
959
                },
11✔
960
        }
11✔
961

11✔
962
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
963
                return err
×
964
        }
×
965

966
        return nil
11✔
967
}
968

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

116✔
973
        var targetNamespace string
116✔
974
        if rel.Spec.TargetNamespace != nil {
232✔
975
                targetNamespace = *rel.Spec.TargetNamespace
116✔
976
        }
116✔
977

978
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
116✔
979
        if err != nil {
126✔
980
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
981
        }
10✔
982

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

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

1020
// buildBootstrapInput constructs the desired BootstrapInput from the current
1021
// target and resolved releases. Used for both comparison and spec construction.
1022
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string, insecure bool) (solarv1alpha1.BootstrapInput, error) {
37✔
1023
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
37✔
1024

37✔
1025
        for _, ri := range releases {
87✔
1026
                if ri.uniqueName == "" {
51✔
1027
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
1028
                }
1✔
1029

1030
                trimmedRef := strings.TrimPrefix(ri.chartURL, "oci://")
49✔
1031
                ref, err := ociname.ParseReference(trimmedRef)
49✔
1032
                if err != nil {
49✔
1033
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
1034
                }
×
1035

1036
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
49✔
1037
                        Repository:     ref.Context().String(),
49✔
1038
                        Tag:            ref.Identifier(),
49✔
1039
                        PullSecretName: renderRegistryPullSecret,
49✔
1040
                        Insecure:       insecure,
49✔
1041
                }
49✔
1042
        }
1043

1044
        return solarv1alpha1.BootstrapInput{
36✔
1045
                Releases: resolvedReleases,
36✔
1046
                Userdata: target.Spec.Userdata,
36✔
1047
        }, nil
36✔
1048
}
1049

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

1056
        releaseNames := make([]string, 0, len(releases))
7✔
1057
        for _, ri := range releases {
16✔
1058
                releaseNames = append(releaseNames, ri.name)
9✔
1059
        }
9✔
1060

1061
        sort.Strings(releaseNames)
7✔
1062

7✔
1063
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
7✔
1064
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
1065
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
7✔
1066

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

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

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

1137
        return false, nil
4✔
1138
}
1139

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

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

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

×
1159
                return nil
×
1160
        }
×
1161

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

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

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

1210
        return requests
210✔
1211
}
1212

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

1226
        type hostEntry struct {
130✔
1227
                pullSecret  string
130✔
1228
                bindingName string
130✔
1229
        }
130✔
1230

130✔
1231
        lookup := make(map[string]hostEntry, len(rbList.Items))
130✔
1232

130✔
1233
        for _, rb := range rbList.Items {
171✔
1234
                reg := &solarv1alpha1.Registry{}
41✔
1235
                if err := r.Get(ctx, client.ObjectKey{
41✔
1236
                        Name:      rb.Spec.RegistryRef.Name,
41✔
1237
                        Namespace: rb.Namespace,
41✔
1238
                }, reg); err != nil {
51✔
1239
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1240
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1241
                }
10✔
1242

1243
                host := strings.ToLower(reg.Spec.Hostname)
31✔
1244
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
42✔
1245
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
11✔
1246
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
11✔
1247
                }
11✔
1248

1249
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
20✔
1250
        }
1251

1252
        result := make(map[string]string, len(lookup))
109✔
1253
        for host, entry := range lookup {
118✔
1254
                result[host] = entry.pullSecret
9✔
1255
        }
9✔
1256

1257
        return result, nil
109✔
1258
}
1259

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

1268
        if rb.Spec.TargetRef.Name == "" {
66✔
1269
                return nil
×
1270
        }
×
1271

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

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

1291
        var requests []reconcile.Request
16✔
1292

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

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

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

1370
        return requests
16✔
1371
}
1372

1373
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1374
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
41✔
1375
        for _, t := range grant.Spec.To {
82✔
1376
                if t.Kind == "Registry" && t.Group == solarGroup {
48✔
1377
                        return true
7✔
1378
                }
7✔
1379
        }
1380

1381
        return false
34✔
1382
}
1383

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

1403
        return false
×
1404
}
1405

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

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

1447
        return result, nil
109✔
1448
}
1449

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

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

×
1465
                return nil
×
1466
        }
×
1467

1468
        seen := map[string]struct{}{}
428✔
1469
        var requests []reconcile.Request
428✔
1470

428✔
1471
        for _, rb := range bindingList.Items {
630✔
1472
                targetNs := rb.Namespace
202✔
1473
                if rb.Spec.TargetRef.Namespace != "" {
202✔
UNCOV
1474
                        targetNs = rb.Spec.TargetRef.Namespace
×
UNCOV
1475
                }
×
1476

1477
                key := targetNs + "/" + rb.Spec.TargetRef.Name
202✔
1478
                if _, ok := seen[key]; ok {
206✔
1479
                        continue
4✔
1480
                }
1481

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

1491
        return requests
428✔
1492
}
1493

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

1500
        targetNs := rb.Namespace
235✔
1501
        if rb.Spec.TargetRef.Namespace != "" {
252✔
1502
                targetNs = rb.Spec.TargetRef.Namespace
17✔
1503
        }
17✔
1504

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

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