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

opendefensecloud / solution-arsenal / 27283699987

10 Jun 2026 02:32PM UTC coverage: 74.207% (+0.2%) from 74.029%
27283699987

Pull #584

github

web-flow
Merge d2ba32b2d into 0ca8f933c
Pull Request #584: feat: improve resource polling and backoff

39 of 44 new or added lines in 2 files covered. (88.64%)

12 existing lines in 2 files now uncovered.

2667 of 3594 relevant lines covered (74.21%)

26.01 hits per line

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

72.17
/pkg/controller/target_controller.go
1
// Copyright 2026 BWI GmbH and Solution Arsenal contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "errors"
9
        "fmt"
10
        "net/url"
11
        "slices"
12
        "sort"
13
        "strings"
14
        "time"
15

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

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

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

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

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

46
type releaseInfo struct {
47
        // bindingKey is "<namespace>/<name>" of the originating ReleaseBinding, used as a
48
        // deterministic tiebreaker when two releases share the same priority.
49
        bindingKey string
50
        name       string
51
        release    *solarv1alpha1.Release
52
        cv         *solarv1alpha1.ComponentVersion
53
        rtName     string
54
        chartURL   string
55
}
56

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

72
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete
73
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch
74
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update
75
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get;list;watch
76
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch
77
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registrybindings,verbs=get;list;watch
78
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
79
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
80
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list;watch
81
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
82
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
83

84
// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release
85
// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask.
86
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
234✔
87
        log := ctrl.LoggerFrom(ctx)
234✔
88

234✔
89
        log.V(1).Info("Target is being reconciled", "req", req)
234✔
90

234✔
91
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
313✔
92
                return ctrl.Result{}, nil
79✔
93
        }
79✔
94

95
        // Fetch target
96
        target := &solarv1alpha1.Target{}
155✔
97
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
158✔
98
                if apierrors.IsNotFound(err) {
6✔
99
                        return ctrl.Result{}, nil
3✔
100
                }
3✔
101

102
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
103
        }
104

105
        // Handle deletion
106
        if !target.DeletionTimestamp.IsZero() {
153✔
107
                log.V(1).Info("Target is being deleted")
1✔
108
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
1✔
109

1✔
110
                // Delete owned RenderTasks
1✔
111
                if err := r.deleteOwnedRenderTasks(ctx, target); err != nil {
1✔
112
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
113
                }
×
114

115
                // Remove finalizer
116
                if slices.Contains(target.Finalizers, targetFinalizer) {
2✔
117
                        latest := &solarv1alpha1.Target{}
1✔
118
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
1✔
119
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
120
                        }
×
121

122
                        original := latest.DeepCopy()
1✔
123
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
2✔
124
                                return s == targetFinalizer
1✔
125
                        })
1✔
126
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
1✔
127
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
128
                        }
×
129
                }
130

131
                return ctrl.Result{}, nil
1✔
132
        }
133

134
        // Set finalizer if not set
135
        if !slices.Contains(target.Finalizers, targetFinalizer) {
182✔
136
                latest := &solarv1alpha1.Target{}
31✔
137
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
31✔
138
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
139
                }
×
140

141
                original := latest.DeepCopy()
31✔
142
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
31✔
143
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
32✔
144
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
1✔
145
                }
1✔
146

147
                return ctrl.Result{}, nil
30✔
148
        }
149

150
        // Resolve render registry — supports cross-namespace via ReferenceGrant
151
        registryNamespace := target.Namespace
120✔
152
        if target.Spec.RenderRegistryNamespace != "" {
120✔
153
                registryNamespace = target.Spec.RenderRegistryNamespace
×
154
        }
×
155

156
        // If the registry lives in a different namespace, verify a ReferenceGrant permits it
157
        // before attempting to fetch the object.
158
        if registryNamespace != target.Namespace {
120✔
159
                granted, err := r.registryGranted(ctx, registryNamespace, target.Namespace)
×
160
                if err != nil {
×
161
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for Registry")
×
162
                }
×
163
                if !granted {
×
164
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotGranted",
×
165
                                "No ReferenceGrant allows access to Registry "+target.Spec.RenderRegistryRef.Name+" in namespace "+registryNamespace); condErr != nil {
×
166
                                return ctrl.Result{}, condErr
×
167
                        }
×
168

NEW
169
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
NEW
170
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
×
171
                }
172
        }
173

174
        registry := &solarv1alpha1.Registry{}
120✔
175
        if err := r.Get(ctx, client.ObjectKey{
120✔
176
                Name:      target.Spec.RenderRegistryRef.Name,
120✔
177
                Namespace: registryNamespace,
120✔
178
        }, registry); err != nil {
141✔
179
                if apierrors.IsNotFound(err) {
42✔
180
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
21✔
181
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
21✔
182
                                return ctrl.Result{}, condErr
×
183
                        }
×
184

185
                        return ctrl.Result{RequeueAfter: requeueAfterForCondition(
21✔
186
                                apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeRegistryResolved), time.Now())}, nil
21✔
187
                }
188

189
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
190
        }
191

192
        if registry.Spec.SolarSecretRef == nil {
101✔
193
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
194
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
195
                        return ctrl.Result{}, condErr
×
196
                }
×
197

198
                return ctrl.Result{}, nil
2✔
199
        }
200

201
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
97✔
202
                "Registry resolved: "+registry.Name); condErr != nil {
97✔
203
                return ctrl.Result{}, condErr
×
204
        }
×
205

206
        // Build hostname→targetPullSecretName lookup from RegistryBindings for this target.
207
        pullSecretsByHost, err := r.buildPullSecretsLookup(ctx, target)
97✔
208
        if err != nil {
117✔
209
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "RegistryBindingConflict",
20✔
210
                        err.Error()); condErr != nil {
20✔
211
                        return ctrl.Result{}, condErr
×
212
                }
×
213

214
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to build pull secrets lookup from RegistryBindings")
20✔
215
        }
216

217
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
218
        // Filter on targetNamespace="" to exclude cross-namespace bindings (targetNamespace set) that share the
219
        // target name but point to a target in a different namespace.
220
        bindingList := &solarv1alpha1.ReleaseBindingList{}
77✔
221
        if err := r.List(ctx, bindingList,
77✔
222
                client.InNamespace(target.Namespace),
77✔
223
                client.MatchingFields{
77✔
224
                        indexReleaseBindingTargetName:      target.Name,
77✔
225
                        indexReleaseBindingTargetNamespace: "",
77✔
226
                },
77✔
227
        ); err != nil {
77✔
228
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
229
        }
×
230

231
        // Collect cross-namespace ReleaseBindings authorized by ReferenceGrants in target's namespace.
232
        crossNsBindings, crossNsErr := r.collectCrossNamespaceReleaseBindings(ctx, target)
77✔
233
        if crossNsErr != nil {
77✔
234
                return ctrl.Result{}, errLogAndWrap(log, crossNsErr, "failed to collect cross-namespace ReleaseBindings")
×
235
        }
×
236
        bindingList.Items = append(bindingList.Items, crossNsBindings...)
77✔
237

77✔
238
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
77✔
239
        // RegistryBinding collection is wired into the rendering pipeline.
77✔
240

77✔
241
        if len(bindingList.Items) == 0 {
87✔
242
                log.V(1).Info("No ReleaseBindings found for target")
10✔
243
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
10✔
244
                        "No ReleaseBindings found for this target"); condErr != nil {
10✔
245
                        return ctrl.Result{}, condErr
×
246
                }
×
247

248
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
10✔
249
                        "No ReleaseBindings found for this target"); condErr != nil {
10✔
250
                        return ctrl.Result{}, condErr
×
251
                }
×
252

253
                return ctrl.Result{}, nil
10✔
254
        }
255

256
        // For each bound release, ensure a per-release RenderTask exists
257
        var releases []releaseInfo
67✔
258

67✔
259
        pendingDeps := false
67✔
260

67✔
261
        for _, binding := range bindingList.Items {
151✔
262
                rel := &solarv1alpha1.Release{}
84✔
263
                if err := r.Get(ctx, client.ObjectKey{
84✔
264
                        Name:      binding.Spec.ReleaseRef.Name,
84✔
265
                        Namespace: binding.Namespace,
84✔
266
                }, rel); err != nil {
84✔
267
                        if apierrors.IsNotFound(err) {
×
268
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
269
                                pendingDeps = true
×
270

×
271
                                continue
×
272
                        }
273

274
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
275
                }
276

277
                cv := &solarv1alpha1.ComponentVersion{}
84✔
278
                cvNamespace := rel.Namespace
84✔
279
                if rel.Spec.ComponentVersionNamespace != "" {
84✔
280
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
281
                }
×
282

283
                if cvNamespace != rel.Namespace {
84✔
284
                        granted := false
×
285
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
286
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
287
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
288
                        }
×
289
                        for i := range grantList.Items {
×
290
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
291
                                        granted = true
×
292
                                        break
×
293
                                }
294
                        }
295
                        if !granted {
×
296
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
297
                                pendingDeps = true
×
298

×
299
                                continue
×
300
                        }
301
                }
302

303
                if err := r.Get(ctx, client.ObjectKey{
84✔
304
                        Name:      rel.Spec.ComponentVersionRef.Name,
84✔
305
                        Namespace: cvNamespace,
84✔
306
                }, cv); err != nil {
84✔
307
                        if apierrors.IsNotFound(err) {
×
308
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
309
                                pendingDeps = true
×
310

×
311
                                continue
×
312
                        }
313

314
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
315
                }
316

317
                rtName := releaseRenderTaskName(rel.Namespace, rel.Name, target.Name, rel.GetGeneration())
84✔
318
                releases = append(releases, releaseInfo{
84✔
319
                        bindingKey: binding.Namespace + "/" + binding.Name,
84✔
320
                        name:       rel.Name,
84✔
321
                        release:    rel,
84✔
322
                        cv:         cv,
84✔
323
                        rtName:     rtName,
84✔
324
                })
84✔
325
        }
326

327
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
328
        var skipped []string
67✔
329
        releases, skipped = resolveReleaseConflicts(releases)
67✔
330
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
67✔
331
                return ctrl.Result{}, condErr
×
332
        }
×
333

334
        if len(releases) == 0 && !pendingDeps {
67✔
335
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
336
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
337
                        return ctrl.Result{}, condErr
×
338
                }
×
339

340
                return ctrl.Result{}, nil
×
341
        }
342

343
        // Create per-release RenderTasks (one per target+release pair).
344
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
345
        allRendered := true
67✔
346

67✔
347
        for i, ri := range releases {
142✔
348
                rt := &solarv1alpha1.RenderTask{}
75✔
349
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
75✔
350

75✔
351
                switch {
75✔
352
                case apierrors.IsNotFound(err):
24✔
353
                        spec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
24✔
354
                        if specErr != nil {
34✔
355
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
10✔
356
                                        specErr.Error()); condErr != nil {
10✔
357
                                        return ctrl.Result{}, condErr
×
358
                                }
×
359

360
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
10✔
361
                        }
362

363
                        rt = &solarv1alpha1.RenderTask{
14✔
364
                                ObjectMeta: metav1.ObjectMeta{
14✔
365
                                        Name:      ri.rtName,
14✔
366
                                        Namespace: target.Namespace,
14✔
367
                                },
14✔
368
                                Spec: spec,
14✔
369
                        }
14✔
370

14✔
371
                        if err := r.Create(ctx, rt); err != nil {
14✔
372
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
373
                        }
×
374

375
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
14✔
376
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
14✔
377
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
14✔
378
                case err != nil:
×
379
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
380
                default:
51✔
381
                        // RenderTask exists — check for spec drift (e.g. pull secrets
51✔
382
                        // changed after a RegistryBinding was created/updated).
51✔
383
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
51✔
384
                        if specErr != nil {
51✔
385
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
×
386
                                        specErr.Error()); condErr != nil {
×
387
                                        return ctrl.Result{}, condErr
×
388
                                }
×
389

390
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
391
                        }
392

393
                        if !apiequality.Semantic.DeepEqual(rt.Spec, desiredSpec) {
53✔
394
                                if err := r.Delete(ctx, rt); err != nil {
2✔
395
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete stale release RenderTask")
×
396
                                }
×
397

398
                                rt = &solarv1alpha1.RenderTask{
2✔
399
                                        ObjectMeta: metav1.ObjectMeta{
2✔
400
                                                Name:      ri.rtName,
2✔
401
                                                Namespace: target.Namespace,
2✔
402
                                        },
2✔
403
                                        Spec: desiredSpec,
2✔
404
                                }
2✔
405

2✔
406
                                if err := r.Create(ctx, rt); err != nil {
2✔
407
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to recreate release RenderTask")
×
408
                                }
×
409

410
                                log.V(1).Info("Recreated release RenderTask (spec drift)", "release", ri.name, "renderTask", ri.rtName)
2✔
411
                                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update",
2✔
412
                                        "Recreated release RenderTask %s for release %s (spec drift)", ri.rtName, ri.name)
2✔
413
                        }
414
                }
415

416
                // Check if release RenderTask is complete
417
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
65✔
418
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
419
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
420
                                return ctrl.Result{}, condErr
×
421
                        }
×
422

423
                        return ctrl.Result{}, nil
×
424
                }
425

426
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
83✔
427
                        releases[i].chartURL = rt.Status.ChartURL
18✔
428
                } else {
65✔
429
                        allRendered = false
47✔
430
                }
47✔
431
        }
432

433
        if pendingDeps {
57✔
434
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
435
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
436
                        return ctrl.Result{}, condErr
×
437
                }
×
438

NEW
439
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
×
NEW
440
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
×
441
        }
442

443
        if !allRendered {
104✔
444
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
47✔
445
                        "Waiting for release RenderTasks to complete"); condErr != nil {
47✔
UNCOV
446
                        return ctrl.Result{}, condErr
×
UNCOV
447
                }
×
448

449
                return ctrl.Result{RequeueAfter: requeueAfterForCondition(
47✔
450
                        apimeta.FindStatusCondition(target.Status.Conditions, ConditionTypeReleasesRendered), time.Now())}, nil
47✔
451
        }
452

453
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
10✔
454
                "All releases rendered successfully"); condErr != nil {
10✔
455
                return ctrl.Result{}, condErr
×
456
        }
×
457

458
        // Determine if a new bootstrap render is needed by checking whether the
459
        // current bootstrapVersion's RenderTask still matches the desired release set.
460
        bootstrapVersion := target.Status.BootstrapVersion
10✔
461
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
10✔
462
        bootstrapRT := &solarv1alpha1.RenderTask{}
10✔
463
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
10✔
464

10✔
465
        needsNewBootstrap := false
10✔
466

10✔
467
        switch {
10✔
468
        case apierrors.IsNotFound(err):
1✔
469
                // No RenderTask for the current version yet — create one
1✔
470
                needsNewBootstrap = true
1✔
471
        case err != nil:
×
472
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
473
        default:
9✔
474
                // RenderTask exists — check if the desired bootstrap input changed
9✔
475
                // (release set, resolved refs/tags, or userdata)
9✔
476
                desiredInput, inputErr := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
9✔
477
                if inputErr != nil {
9✔
478
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
479
                }
×
480

481
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
9✔
482
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
10✔
483
                        bootstrapVersion++
1✔
484
                        needsNewBootstrap = true
1✔
485
                }
1✔
486
        }
487

488
        if needsNewBootstrap {
12✔
489
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
490
                if specErr != nil {
2✔
491
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
492
                }
×
493

494
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
495
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
496
                        ObjectMeta: metav1.ObjectMeta{
2✔
497
                                Name:      bootstrapRTName,
2✔
498
                                Namespace: target.Namespace,
2✔
499
                        },
2✔
500
                        Spec: spec,
2✔
501
                }
2✔
502

2✔
503
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
504
                        if !apierrors.IsAlreadyExists(err) {
×
505
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
506
                        }
×
507

508
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
509
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
510
                        }
×
511
                } else {
2✔
512
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
513
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
514
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
515
                }
2✔
516

517
                // Persist the new bootstrapVersion in status
518
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
519
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
520
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
521
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
522
                        }
×
523
                }
524
        }
525

526
        // Update target status from bootstrap RenderTask
527
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
10✔
528
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
529
                        "Bootstrap rendering failed"); condErr != nil {
×
530
                        return ctrl.Result{}, condErr
×
531
                }
×
532

533
                return ctrl.Result{}, nil
×
534
        }
535

536
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
14✔
537
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
4✔
538
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
4✔
539
                        return ctrl.Result{}, condErr
×
540
                }
×
541

542
                // Clean up stale RenderTasks owned by this target (old versions)
543
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
4✔
544
                for _, ri := range releases {
10✔
545
                        currentRTNames[ri.rtName] = struct{}{}
6✔
546
                }
6✔
547
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
4✔
548
                        log.Error(err, "failed to clean up stale RenderTasks")
×
549
                }
×
550

551
                return ctrl.Result{}, nil
4✔
552
        }
553

554
        // Still running
555
        return ctrl.Result{}, nil
6✔
556
}
557

558
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
298✔
559
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
298✔
560
                Type:               condType,
298✔
561
                Status:             status,
298✔
562
                ObservedGeneration: target.Generation,
298✔
563
                Reason:             reason,
298✔
564
                Message:            message,
298✔
565
        })
298✔
566
        if changed {
375✔
567
                if err := r.Status().Update(ctx, target); err != nil {
77✔
UNCOV
568
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
×
UNCOV
569
                }
×
570
        }
571

572
        return nil
298✔
573
}
574

575
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
67✔
576
        if len(skipped) == 0 {
125✔
577
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
58✔
578
        }
58✔
579

580
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
581
}
582

583
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
584
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
585
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
586
// It returns the accepted releases and a slice of human-readable filter messages.
587
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
75✔
588
        if len(releases) == 0 {
76✔
589
                return releases, nil
1✔
590
        }
1✔
591

592
        // Step A: uniqueName deduplication.
593
        // When UniqueName is empty, fall back to the parent Component name from the CV.
594
        namedGroups := map[string][]releaseInfo{}
74✔
595

74✔
596
        for _, ri := range releases {
170✔
597
                uniqueName := ri.release.Spec.UniqueName
96✔
598
                if uniqueName == "" {
102✔
599
                        uniqueName = ri.cv.Spec.ComponentRef.Name
6✔
600
                }
6✔
601

602
                namedGroups[uniqueName] = append(namedGroups[uniqueName], ri)
96✔
603
        }
604

605
        var accepted []releaseInfo
74✔
606

74✔
607
        var skipped []string
74✔
608

74✔
609
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
74✔
610
        byPriority := func(a, b releaseInfo) bool {
96✔
611
                if a.release.Spec.Priority != b.release.Spec.Priority {
32✔
612
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
613
                }
10✔
614

615
                return a.bindingKey < b.bindingKey
12✔
616
        }
617

618
        uniqueNames := make([]string, 0, len(namedGroups))
74✔
619
        for k := range namedGroups {
161✔
620
                uniqueNames = append(uniqueNames, k)
87✔
621
        }
87✔
622

623
        sort.Strings(uniqueNames)
74✔
624

74✔
625
        for _, uniqueName := range uniqueNames {
161✔
626
                group := namedGroups[uniqueName]
87✔
627
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
96✔
628

629
                accepted = append(accepted, group[0])
87✔
630

87✔
631
                for _, loser := range group[1:] {
96✔
632
                        skipped = append(skipped, fmt.Sprintf(
9✔
633
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
634
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
635
                        ))
9✔
636
                }
9✔
637
        }
638

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

644
        resolved := make([]releaseInfo, 0, len(accepted))
74✔
645

74✔
646
        for _, ri := range accepted {
161✔
647
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
87✔
648
                var riSelector labels.Selector
87✔
649
                if ri.release.Spec.AntiAffinity != nil {
94✔
650
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
651
                        if err != nil {
8✔
652
                                skipped = append(skipped, fmt.Sprintf(
1✔
653
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
654
                                        ri.bindingKey, err,
1✔
655
                                ))
1✔
656

1✔
657
                                continue
1✔
658
                        }
659

660
                        riSelector = sel
6✔
661
                }
662

663
                // Check both directions: ri's anti-affinity against already-resolved labels,
664
                // and already-resolved anti-affinities against ri's labels.
665
                conflict := ""
86✔
666
                for _, other := range resolved {
99✔
667
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
17✔
668
                                conflict = other.bindingKey
4✔
669
                                break
4✔
670
                        }
671

672
                        if other.release.Spec.AntiAffinity != nil {
10✔
673
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
674
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
675
                                        conflict = other.bindingKey
1✔
676
                                        break
1✔
677
                                }
678
                        }
679
                }
680

681
                if conflict != "" {
91✔
682
                        skipped = append(skipped, fmt.Sprintf(
5✔
683
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
684
                                ri.bindingKey, conflict,
5✔
685
                        ))
5✔
686
                } else {
86✔
687
                        resolved = append(resolved, ri)
81✔
688
                }
81✔
689
        }
690

691
        return resolved, skipped
74✔
692
}
693

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

4✔
700
        rtList := &solarv1alpha1.RenderTaskList{}
4✔
701
        if err := r.List(ctx, rtList,
4✔
702
                client.InNamespace(target.Namespace),
4✔
703
                client.MatchingFields{indexOwnerKind: "Target"},
4✔
704
        ); err != nil {
4✔
705
                return err
×
706
        }
×
707

708
        for i := range rtList.Items {
15✔
709
                rt := &rtList.Items[i]
11✔
710
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
11✔
711
                        continue
×
712
                }
713

714
                if _, current := currentRTNames[rt.Name]; current {
21✔
715
                        continue
10✔
716
                }
717

718
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
719
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
720
                        return err
×
721
                }
×
722

723
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
724
                        "Deleted stale RenderTask %s", rt.Name)
1✔
725
        }
726

727
        return nil
4✔
728
}
729

730
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
731
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
732
        if err := r.List(ctx, rtList,
1✔
733
                client.InNamespace(target.Namespace),
1✔
734
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
735
        ); err != nil {
1✔
736
                return err
×
737
        }
×
738

739
        for i := range rtList.Items {
1✔
740
                rt := &rtList.Items[i]
×
741
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
742
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
743
                                return err
×
744
                        }
×
745
                }
746
        }
747

748
        return nil
1✔
749
}
750

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

75✔
755
        var targetNamespace string
75✔
756
        if rel.Spec.TargetNamespace != nil {
150✔
757
                targetNamespace = *rel.Spec.TargetNamespace
75✔
758
        }
75✔
759

760
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
75✔
761
        if err != nil {
85✔
762
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
763
        }
10✔
764

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

65✔
771
        return solarv1alpha1.RenderTaskSpec{
65✔
772
                RendererConfig: solarv1alpha1.RendererConfig{
65✔
773
                        Type: solarv1alpha1.RendererConfigTypeRelease,
65✔
774
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
65✔
775
                                Chart: solarv1alpha1.ChartConfig{
65✔
776
                                        Name:        chartName,
65✔
777
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
65✔
778
                                        Version:     tag,
65✔
779
                                        AppVersion:  tag,
65✔
780
                                },
65✔
781
                                Input: solarv1alpha1.ReleaseInput{
65✔
782
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
65✔
783
                                        Resources:  resolvedResources,
65✔
784
                                        Entrypoint: cv.Spec.Entrypoint,
65✔
785
                                },
65✔
786
                                Values:          rel.Spec.Values,
65✔
787
                                TargetNamespace: targetNamespace,
65✔
788
                        },
65✔
789
                },
65✔
790
                Repository:     repo,
65✔
791
                Tag:            tag,
65✔
792
                BaseURL:        registry.Spec.Hostname,
65✔
793
                PushSecretRef:  registry.Spec.SolarSecretRef,
65✔
794
                FailedJobTTL:   rel.Spec.FailedJobTTL,
65✔
795
                OwnerName:      target.Name,
65✔
796
                OwnerNamespace: target.Namespace,
65✔
797
                OwnerKind:      "Target",
65✔
798
        }, nil
65✔
799
}
800

801
// buildBootstrapInput constructs the desired BootstrapInput from the current
802
// target and resolved releases. Used for both comparison and spec construction.
803
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string) (solarv1alpha1.BootstrapInput, error) {
11✔
804
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
11✔
805

11✔
806
        for _, ri := range releases {
28✔
807
                ref, err := ociname.ParseReference(ri.chartURL)
17✔
808
                if err != nil {
17✔
809
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
810
                }
×
811

812
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
17✔
813
                if err != nil {
17✔
814
                        return solarv1alpha1.BootstrapInput{}, err
×
815
                }
×
816

817
                resolvedReleases[ri.name] = solarv1alpha1.ResolvedResourceAccess{
17✔
818
                        Repository:     strings.TrimPrefix(repo, "oci://"),
17✔
819
                        Tag:            ref.Identifier(),
17✔
820
                        PullSecretName: renderRegistryPullSecret,
17✔
821
                }
17✔
822
        }
823

824
        return solarv1alpha1.BootstrapInput{
11✔
825
                Releases: resolvedReleases,
11✔
826
                Userdata: target.Spec.Userdata,
11✔
827
        }, nil
11✔
828
}
829

830
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
2✔
831
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
2✔
832
        if err != nil {
2✔
833
                return solarv1alpha1.RenderTaskSpec{}, err
×
834
        }
×
835

836
        releaseNames := make([]string, 0, len(releases))
2✔
837
        for _, ri := range releases {
5✔
838
                releaseNames = append(releaseNames, ri.name)
3✔
839
        }
3✔
840

841
        sort.Strings(releaseNames)
2✔
842

2✔
843
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
2✔
844
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
2✔
845
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
2✔
846

2✔
847
        return solarv1alpha1.RenderTaskSpec{
2✔
848
                RendererConfig: solarv1alpha1.RendererConfig{
2✔
849
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
2✔
850
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
2✔
851
                                Chart: solarv1alpha1.ChartConfig{
2✔
852
                                        Name:        chartName,
2✔
853
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
2✔
854
                                        Version:     tag,
2✔
855
                                        AppVersion:  tag,
2✔
856
                                },
2✔
857
                                Input: input,
2✔
858
                        },
2✔
859
                },
2✔
860
                Repository:     repo,
2✔
861
                Tag:            tag,
2✔
862
                BaseURL:        registry.Spec.Hostname,
2✔
863
                PushSecretRef:  registry.Spec.SolarSecretRef,
2✔
864
                OwnerName:      target.Name,
2✔
865
                OwnerNamespace: target.Namespace,
2✔
866
                OwnerKind:      "Target",
2✔
867
        }, nil
2✔
868
}
869

870
// SetupWithManager sets up the controller with the Manager.
871
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
872
        return ctrl.NewControllerManagedBy(mgr).
1✔
873
                For(&solarv1alpha1.Target{}).
1✔
874
                Watches(
1✔
875
                        &solarv1alpha1.ReleaseBinding{},
1✔
876
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
877
                ).
1✔
878
                Watches(
1✔
879
                        &solarv1alpha1.RenderTask{},
1✔
880
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
881
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
882
                ).
1✔
883
                Watches(
1✔
884
                        &solarv1alpha1.Registry{},
1✔
885
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
886
                ).
1✔
887
                Watches(
1✔
888
                        &solarv1alpha1.RegistryBinding{},
1✔
889
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
890
                ).
1✔
891
                Watches(
1✔
892
                        &solarv1alpha1.ReferenceGrant{},
1✔
893
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
894
                ).
1✔
895
                Watches(
1✔
896
                        &solarv1alpha1.Release{},
1✔
897
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
898
                ).
1✔
899
                Complete(r)
1✔
900
}
1✔
901

902
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
903
// fromNamespace to reference the named registry.
904
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
905
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
906
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
907
                return false, err
×
908
        }
×
909
        for i := range grantList.Items {
×
910
                grant := &grantList.Items[i]
×
911
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
912
                        return true, nil
×
913
                }
×
914
        }
915

916
        return false, nil
×
917
}
918

919
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
920
// fromNamespace to reference Registry resources in the grant's namespace.
921
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
922
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
923
}
×
924

925
// mapRegistryToTargets maps a Registry event to reconcile requests for all
926
// Targets that reference it — either in the same namespace or cross-namespace.
927
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
27✔
928
        reg, ok := obj.(*solarv1alpha1.Registry)
27✔
929
        if !ok {
27✔
930
                return nil
×
931
        }
×
932

933
        // Same-namespace targets
934
        targetList := &solarv1alpha1.TargetList{}
27✔
935
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
27✔
936
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
937

×
938
                return nil
×
939
        }
×
940

941
        var requests []reconcile.Request
27✔
942
        for _, t := range targetList.Items {
28✔
943
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
1✔
944
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
1✔
945
                        requests = append(requests, reconcile.Request{
×
946
                                NamespacedName: types.NamespacedName{
×
947
                                        Name:      t.Name,
×
948
                                        Namespace: t.Namespace,
×
949
                                },
×
950
                        })
×
951
                }
×
952
        }
953

954
        // Cross-namespace targets: find namespaces that have been granted access to
955
        // registries in reg.Namespace, then check their targets.
956
        grantList := &solarv1alpha1.ReferenceGrantList{}
27✔
957
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
27✔
958
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
959
                return requests
×
960
        }
×
961

962
        for i := range grantList.Items {
27✔
963
                grant := &grantList.Items[i]
×
964
                if !grantsRegistryResource(grant) {
×
965
                        continue
×
966
                }
967
                for _, from := range grant.Spec.From {
×
968
                        if from.Kind != "Target" || from.Group != solarGroup {
×
969
                                continue
×
970
                        }
971
                        crossTargets := &solarv1alpha1.TargetList{}
×
972
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
973
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
974
                                continue
×
975
                        }
976
                        for _, t := range crossTargets.Items {
×
977
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
978
                                        requests = append(requests, reconcile.Request{
×
979
                                                NamespacedName: types.NamespacedName{
×
980
                                                        Name:      t.Name,
×
981
                                                        Namespace: t.Namespace,
×
982
                                                },
×
983
                                        })
×
984
                                }
×
985
                        }
986
                }
987
        }
988

989
        return requests
27✔
990
}
991

992
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
993
// each bound Registry, and returns a map from registry hostname to
994
// targetPullSecretName. Registries without a targetPullSecretName are included
995
// with an empty string (anonymous pull).
996
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
97✔
997
        rbList := &solarv1alpha1.RegistryBindingList{}
97✔
998
        if err := r.List(ctx, rbList,
97✔
999
                client.InNamespace(target.Namespace),
97✔
1000
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
97✔
1001
        ); err != nil {
97✔
1002
                return nil, err
×
1003
        }
×
1004

1005
        type hostEntry struct {
97✔
1006
                pullSecret  string
97✔
1007
                bindingName string
97✔
1008
        }
97✔
1009

97✔
1010
        lookup := make(map[string]hostEntry, len(rbList.Items))
97✔
1011

97✔
1012
        for _, rb := range rbList.Items {
136✔
1013
                reg := &solarv1alpha1.Registry{}
39✔
1014
                if err := r.Get(ctx, client.ObjectKey{
39✔
1015
                        Name:      rb.Spec.RegistryRef.Name,
39✔
1016
                        Namespace: rb.Namespace,
39✔
1017
                }, reg); err != nil {
49✔
1018
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1019
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1020
                }
10✔
1021

1022
                host := strings.ToLower(reg.Spec.Hostname)
29✔
1023
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
39✔
1024
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
10✔
1025
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1026
                }
10✔
1027

1028
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
19✔
1029
        }
1030

1031
        result := make(map[string]string, len(lookup))
77✔
1032
        for host, entry := range lookup {
86✔
1033
                result[host] = entry.pullSecret
9✔
1034
        }
9✔
1035

1036
        return result, nil
77✔
1037
}
1038

1039
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1040
// for the referenced Target.
1041
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
7✔
1042
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
7✔
1043
        if !ok {
7✔
1044
                return nil
×
1045
        }
×
1046

1047
        if rb.Spec.TargetRef.Name == "" {
7✔
1048
                return nil
×
1049
        }
×
1050

1051
        return []reconcile.Request{
7✔
1052
                {
7✔
1053
                        NamespacedName: types.NamespacedName{
7✔
1054
                                Name:      rb.Spec.TargetRef.Name,
7✔
1055
                                Namespace: rb.Namespace,
7✔
1056
                        },
7✔
1057
                },
7✔
1058
        }
7✔
1059
}
1060

1061
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1062
// either because the grant controls Registry access (Target → Registry) or because
1063
// it controls ComponentVersion access (Release → ComponentVersion).
1064
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
13✔
1065
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
13✔
1066
        if !ok {
13✔
1067
                return nil
×
1068
        }
×
1069

1070
        var requests []reconcile.Request
13✔
1071

13✔
1072
        if grantsRegistryResource(grant) {
13✔
1073
                for _, from := range grant.Spec.From {
×
1074
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1075
                                continue
×
1076
                        }
1077
                        targets := &solarv1alpha1.TargetList{}
×
1078
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
1079
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1080
                                continue
×
1081
                        }
1082
                        for _, t := range targets.Items {
×
1083
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
1084
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
1085
                                        requests = append(requests, reconcile.Request{
×
1086
                                                NamespacedName: types.NamespacedName{
×
1087
                                                        Name:      t.Name,
×
1088
                                                        Namespace: t.Namespace,
×
1089
                                                },
×
1090
                                        })
×
1091
                                }
×
1092
                        }
1093
                }
1094
        }
1095

1096
        if grantsComponentVersionResource(grant) {
18✔
1097
                seen := map[string]struct{}{}
5✔
1098
                for _, from := range grant.Spec.From {
10✔
1099
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
1100
                                continue
×
1101
                        }
1102
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
1103
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
1104
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1105
                                continue
×
1106
                        }
1107
                        for _, rb := range bindings.Items {
6✔
1108
                                if rb.Spec.TargetRef.Name == "" {
1✔
1109
                                        continue
×
1110
                                }
1111
                                targetNs := rb.Namespace
1✔
1112
                                if rb.Spec.TargetNamespace != "" {
2✔
1113
                                        targetNs = rb.Spec.TargetNamespace
1✔
1114
                                }
1✔
1115
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1116
                                if _, ok := seen[key]; ok {
1✔
1117
                                        continue
×
1118
                                }
1119
                                seen[key] = struct{}{}
1✔
1120
                                requests = append(requests, reconcile.Request{
1✔
1121
                                        NamespacedName: types.NamespacedName{
1✔
1122
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1123
                                                Namespace: targetNs,
1✔
1124
                                        },
1✔
1125
                                })
1✔
1126
                        }
1127
                }
1128
        }
1129

1130
        if grantsReleaseBindingToTargetResource(grant) {
17✔
1131
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1132
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1133
                // the new or removed cross-namespace ReleaseBindings.
4✔
1134
                targets := &solarv1alpha1.TargetList{}
4✔
1135
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1136
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1137
                } else {
4✔
1138
                        for _, t := range targets.Items {
8✔
1139
                                requests = append(requests, reconcile.Request{
4✔
1140
                                        NamespacedName: types.NamespacedName{
4✔
1141
                                                Name:      t.Name,
4✔
1142
                                                Namespace: t.Namespace,
4✔
1143
                                        },
4✔
1144
                                })
4✔
1145
                        }
4✔
1146
                }
1147
        }
1148

1149
        return requests
13✔
1150
}
1151

1152
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1153
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
13✔
1154
        for _, t := range grant.Spec.To {
26✔
1155
                if t.Kind == "Registry" && t.Group == solarGroup {
13✔
1156
                        return true
×
1157
                }
×
1158
        }
1159

1160
        return false
13✔
1161
}
1162

1163
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1164
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1165
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
26✔
1166
        hasReleaseBindingFrom := false
26✔
1167
        for _, f := range grant.Spec.From {
52✔
1168
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
43✔
1169
                        hasReleaseBindingFrom = true
17✔
1170
                        break
17✔
1171
                }
1172
        }
1173
        if !hasReleaseBindingFrom {
35✔
1174
                return false
9✔
1175
        }
9✔
1176
        for _, t := range grant.Spec.To {
34✔
1177
                if t.Kind == "Target" && t.Group == solarGroup {
34✔
1178
                        return true
17✔
1179
                }
17✔
1180
        }
1181

1182
        return false
×
1183
}
1184

1185
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1186
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1187
// a ReferenceGrant in target's namespace.
1188
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
77✔
1189
        grantList := &solarv1alpha1.ReferenceGrantList{}
77✔
1190
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
77✔
1191
                return nil, err
×
1192
        }
×
1193

1194
        seen := make(map[string]struct{})
77✔
1195
        var result []solarv1alpha1.ReleaseBinding
77✔
1196
        for i := range grantList.Items {
90✔
1197
                grant := &grantList.Items[i]
13✔
1198
                if !grantsReleaseBindingToTargetResource(grant) {
13✔
1199
                        continue
×
1200
                }
1201
                for _, from := range grant.Spec.From {
26✔
1202
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
13✔
1203
                                continue
×
1204
                        }
1205
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
13✔
1206
                        if err := r.List(ctx, crossBindings,
13✔
1207
                                client.InNamespace(from.Namespace),
13✔
1208
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
13✔
1209
                        ); err != nil {
13✔
1210
                                return nil, err
×
1211
                        }
×
1212
                        for _, rb := range crossBindings.Items {
23✔
1213
                                if rb.Spec.TargetNamespace != target.Namespace {
10✔
1214
                                        continue
×
1215
                                }
1216
                                key := rb.Namespace + "/" + rb.Name
10✔
1217
                                if _, exists := seen[key]; exists {
11✔
1218
                                        continue
1✔
1219
                                }
1220
                                seen[key] = struct{}{}
9✔
1221
                                result = append(result, rb)
9✔
1222
                        }
1223
                }
1224
        }
1225

1226
        return result, nil
77✔
1227
}
1228

1229
// mapReleaseToTargets maps a Release event to reconcile requests for all
1230
// Targets that are bound to the release via ReleaseBindings.
1231
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
90✔
1232
        rel, ok := obj.(*solarv1alpha1.Release)
90✔
1233
        if !ok {
90✔
1234
                return nil
×
1235
        }
×
1236

1237
        bindingList := &solarv1alpha1.ReleaseBindingList{}
90✔
1238
        if err := r.List(ctx, bindingList,
90✔
1239
                client.InNamespace(rel.Namespace),
90✔
1240
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
90✔
1241
        ); err != nil {
90✔
1242
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1243

×
1244
                return nil
×
1245
        }
×
1246

1247
        seen := map[string]struct{}{}
90✔
1248
        var requests []reconcile.Request
90✔
1249

90✔
1250
        for _, rb := range bindingList.Items {
92✔
1251
                targetNs := rb.Namespace
2✔
1252
                if rb.Spec.TargetNamespace != "" {
2✔
1253
                        targetNs = rb.Spec.TargetNamespace
×
1254
                }
×
1255

1256
                key := targetNs + "/" + rb.Spec.TargetRef.Name
2✔
1257
                if _, ok := seen[key]; ok {
2✔
1258
                        continue
×
1259
                }
1260

1261
                seen[key] = struct{}{}
2✔
1262
                requests = append(requests, reconcile.Request{
2✔
1263
                        NamespacedName: types.NamespacedName{
2✔
1264
                                Name:      rb.Spec.TargetRef.Name,
2✔
1265
                                Namespace: targetNs,
2✔
1266
                        },
2✔
1267
                })
2✔
1268
        }
1269

1270
        return requests
90✔
1271
}
1272

1273
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
33✔
1274
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
33✔
1275
        if !ok || rb.Spec.TargetRef.Name == "" {
33✔
1276
                return nil
×
1277
        }
×
1278

1279
        targetNs := rb.Namespace
33✔
1280
        if rb.Spec.TargetNamespace != "" {
42✔
1281
                targetNs = rb.Spec.TargetNamespace
9✔
1282
        }
9✔
1283

1284
        return []reconcile.Request{
33✔
1285
                {
33✔
1286
                        NamespacedName: types.NamespacedName{
33✔
1287
                                Name:      rb.Spec.TargetRef.Name,
33✔
1288
                                Namespace: targetNs,
33✔
1289
                        },
33✔
1290
                },
33✔
1291
        }
33✔
1292
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc