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

opendefensecloud / solution-arsenal / 26148199291

20 May 2026 07:31AM UTC coverage: 72.073% (-0.6%) from 72.713%
26148199291

Pull #534

github

web-flow
Merge a20b5e3cb into 7e21d7039
Pull Request #534: fix(deps): update k8s.io/kube-openapi digest to aa012df

2364 of 3280 relevant lines covered (72.07%)

32.36 hits per line

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

69.06
/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
}
66

67
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets,verbs=get;list;watch;create;update;patch;delete
68
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/status,verbs=get;update;patch
69
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=targets/finalizers,verbs=update
70
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=registries,verbs=get;list;watch
71
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releasebindings,verbs=get;list;watch
72
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
73
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
74
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=referencegrants,verbs=get;list;watch
75
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
76
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
77

78
// Reconcile collects ReleaseBindings, resolves the render registry, creates per-release
79
// RenderTasks (with dedup), and creates a per-target bootstrap RenderTask.
80
func (r *TargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
133✔
81
        log := ctrl.LoggerFrom(ctx)
133✔
82

133✔
83
        log.V(1).Info("Target is being reconciled", "req", req)
133✔
84

133✔
85
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
178✔
86
                return ctrl.Result{}, nil
45✔
87
        }
45✔
88

89
        // Fetch target
90
        target := &solarv1alpha1.Target{}
88✔
91
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
95✔
92
                if apierrors.IsNotFound(err) {
14✔
93
                        return ctrl.Result{}, nil
7✔
94
                }
7✔
95

96
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
97
        }
98

99
        // Handle deletion
100
        if !target.DeletionTimestamp.IsZero() {
82✔
101
                log.V(1).Info("Target is being deleted")
1✔
102
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
1✔
103

1✔
104
                // Delete owned RenderTasks
1✔
105
                if err := r.deleteOwnedRenderTasks(ctx, target); err != nil {
1✔
106
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderTasks")
×
107
                }
×
108

109
                // Remove finalizer
110
                if slices.Contains(target.Finalizers, targetFinalizer) {
2✔
111
                        latest := &solarv1alpha1.Target{}
1✔
112
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
1✔
113
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
114
                        }
×
115

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

125
                return ctrl.Result{}, nil
1✔
126
        }
127

128
        // Set finalizer if not set
129
        if !slices.Contains(target.Finalizers, targetFinalizer) {
98✔
130
                latest := &solarv1alpha1.Target{}
18✔
131
                if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
18✔
132
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer addition")
×
133
                }
×
134

135
                original := latest.DeepCopy()
18✔
136
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
18✔
137
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
18✔
138
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
139
                }
×
140

141
                return ctrl.Result{}, nil
18✔
142
        }
143

144
        // Resolve render registry — supports cross-namespace via ReferenceGrant
145
        registryNamespace := target.Namespace
62✔
146
        if target.Spec.RenderRegistryNamespace != "" {
62✔
147
                registryNamespace = target.Spec.RenderRegistryNamespace
×
148
        }
×
149

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

163
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
164
                }
165
        }
166

167
        registry := &solarv1alpha1.Registry{}
62✔
168
        if err := r.Get(ctx, client.ObjectKey{
62✔
169
                Name:      target.Spec.RenderRegistryRef.Name,
62✔
170
                Namespace: registryNamespace,
62✔
171
        }, registry); err != nil {
82✔
172
                if apierrors.IsNotFound(err) {
40✔
173
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
20✔
174
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
21✔
175
                                return ctrl.Result{}, condErr
1✔
176
                        }
1✔
177

178
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
19✔
179
                }
180

181
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
182
        }
183

184
        if registry.Spec.SolarSecretRef == nil {
44✔
185
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
186
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
187
                        return ctrl.Result{}, condErr
×
188
                }
×
189

190
                return ctrl.Result{}, nil
2✔
191
        }
192

193
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
40✔
194
                "Registry resolved: "+registry.Name); condErr != nil {
40✔
195
                return ctrl.Result{}, condErr
×
196
        }
×
197

198
        // Collect ReleaseBindings for this target
199
        bindingList := &solarv1alpha1.ReleaseBindingList{}
40✔
200
        if err := r.List(ctx, bindingList,
40✔
201
                client.InNamespace(target.Namespace),
40✔
202
                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
40✔
203
        ); err != nil {
40✔
204
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
205
        }
×
206

207
        if len(bindingList.Items) == 0 {
46✔
208
                log.V(1).Info("No ReleaseBindings found for target")
6✔
209
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
6✔
210
                        "No ReleaseBindings found for this target"); condErr != nil {
6✔
211
                        return ctrl.Result{}, condErr
×
212
                }
×
213

214
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
6✔
215
                        "No ReleaseBindings found for this target"); condErr != nil {
6✔
216
                        return ctrl.Result{}, condErr
×
217
                }
×
218

219
                return ctrl.Result{}, nil
6✔
220
        }
221

222
        // For each bound release, ensure a per-release RenderTask exists
223
        var releases []releaseInfo
34✔
224

34✔
225
        pendingDeps := false
34✔
226

34✔
227
        for _, binding := range bindingList.Items {
86✔
228
                rel := &solarv1alpha1.Release{}
52✔
229
                if err := r.Get(ctx, client.ObjectKey{
52✔
230
                        Name:      binding.Spec.ReleaseRef.Name,
52✔
231
                        Namespace: target.Namespace,
52✔
232
                }, rel); err != nil {
52✔
233
                        if apierrors.IsNotFound(err) {
×
234
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
235
                                pendingDeps = true
×
236

×
237
                                continue
×
238
                        }
239

240
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
241
                }
242

243
                cv := &solarv1alpha1.ComponentVersion{}
52✔
244
                cvNamespace := target.Namespace
52✔
245
                if rel.Spec.ComponentVersionNamespace != "" {
52✔
246
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
247
                }
×
248

249
                if cvNamespace != target.Namespace {
52✔
250
                        granted := false
×
251
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
252
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
253
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
254
                        }
×
255
                        for i := range grantList.Items {
×
256
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
257
                                        granted = true
×
258
                                }
×
259
                        }
260
                        if !granted {
×
261
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
262
                                pendingDeps = true
×
263

×
264
                                continue
×
265
                        }
266
                }
267

268
                if err := r.Get(ctx, client.ObjectKey{
52✔
269
                        Name:      rel.Spec.ComponentVersionRef.Name,
52✔
270
                        Namespace: cvNamespace,
52✔
271
                }, cv); err != nil {
52✔
272
                        if apierrors.IsNotFound(err) {
×
273
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
274
                                pendingDeps = true
×
275

×
276
                                continue
×
277
                        }
278

279
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
280
                }
281

282
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
52✔
283
                releases = append(releases, releaseInfo{
52✔
284
                        bindingKey: binding.Namespace + "/" + binding.Name,
52✔
285
                        name:       rel.Name,
52✔
286
                        release:    rel,
52✔
287
                        cv:         cv,
52✔
288
                        rtName:     rtName,
52✔
289
                })
52✔
290
        }
291

292
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
293
        var skipped []string
34✔
294
        releases, skipped = resolveReleaseConflicts(releases)
34✔
295
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
35✔
296
                return ctrl.Result{}, condErr
1✔
297
        }
1✔
298

299
        if len(releases) == 0 && !pendingDeps {
33✔
300
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
301
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
302
                        return ctrl.Result{}, condErr
×
303
                }
×
304

305
                return ctrl.Result{}, nil
×
306
        }
307

308
        // Create per-release RenderTasks (one per target+release pair).
309
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
310
        allRendered := true
33✔
311

33✔
312
        for i, ri := range releases {
75✔
313
                rt := &solarv1alpha1.RenderTask{}
42✔
314
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
42✔
315

42✔
316
                if apierrors.IsNotFound(err) {
49✔
317
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
7✔
318
                        rt = &solarv1alpha1.RenderTask{
7✔
319
                                ObjectMeta: metav1.ObjectMeta{
7✔
320
                                        Name:      ri.rtName,
7✔
321
                                        Namespace: target.Namespace,
7✔
322
                                },
7✔
323
                                Spec: spec,
7✔
324
                        }
7✔
325

7✔
326
                        if err := r.Create(ctx, rt); err != nil {
7✔
327
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
328
                        }
×
329

330
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
7✔
331
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
332
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
7✔
333
                } else if err != nil {
35✔
334
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
335
                }
×
336

337
                // Check if release RenderTask is complete
338
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
42✔
339
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
340
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
341
                                return ctrl.Result{}, condErr
×
342
                        }
×
343

344
                        return ctrl.Result{}, nil
×
345
                }
346

347
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
62✔
348
                        releases[i].chartURL = rt.Status.ChartURL
20✔
349
                } else {
42✔
350
                        allRendered = false
22✔
351
                }
22✔
352
        }
353

354
        if pendingDeps {
33✔
355
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
356
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
357
                        return ctrl.Result{}, condErr
×
358
                }
×
359

360
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
361
        }
362

363
        if !allRendered {
55✔
364
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
22✔
365
                        "Waiting for release RenderTasks to complete"); condErr != nil {
23✔
366
                        return ctrl.Result{}, condErr
1✔
367
                }
1✔
368

369
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
21✔
370
        }
371

372
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
11✔
373
                "All releases rendered successfully"); condErr != nil {
11✔
374
                return ctrl.Result{}, condErr
×
375
        }
×
376

377
        // Determine if a new bootstrap render is needed by checking whether the
378
        // current bootstrapVersion's RenderTask still matches the desired release set.
379
        bootstrapVersion := target.Status.BootstrapVersion
11✔
380
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
11✔
381
        bootstrapRT := &solarv1alpha1.RenderTask{}
11✔
382
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
11✔
383

11✔
384
        needsNewBootstrap := false
11✔
385

11✔
386
        switch {
11✔
387
        case apierrors.IsNotFound(err):
1✔
388
                // No RenderTask for the current version yet — create one
1✔
389
                needsNewBootstrap = true
1✔
390
        case err != nil:
×
391
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
392
        default:
10✔
393
                // RenderTask exists — check if the desired bootstrap input changed
10✔
394
                // (release set, resolved refs/tags, or userdata)
10✔
395
                desiredInput, inputErr := buildBootstrapInput(target, releases)
10✔
396
                if inputErr != nil {
10✔
397
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
398
                }
×
399

400
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
10✔
401
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
11✔
402
                        bootstrapVersion++
1✔
403
                        needsNewBootstrap = true
1✔
404
                }
1✔
405
        }
406

407
        if needsNewBootstrap {
13✔
408
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
409
                if specErr != nil {
2✔
410
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
411
                }
×
412

413
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
414
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
415
                        ObjectMeta: metav1.ObjectMeta{
2✔
416
                                Name:      bootstrapRTName,
2✔
417
                                Namespace: target.Namespace,
2✔
418
                        },
2✔
419
                        Spec: spec,
2✔
420
                }
2✔
421

2✔
422
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
423
                        if !apierrors.IsAlreadyExists(err) {
×
424
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
425
                        }
×
426

427
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
428
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
429
                        }
×
430
                } else {
2✔
431
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
432
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
433
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
434
                }
2✔
435

436
                // Persist the new bootstrapVersion in status
437
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
438
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
439
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
440
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
441
                        }
×
442
                }
443
        }
444

445
        // Update target status from bootstrap RenderTask
446
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
11✔
447
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
448
                        "Bootstrap rendering failed"); condErr != nil {
×
449
                        return ctrl.Result{}, condErr
×
450
                }
×
451

452
                return ctrl.Result{}, nil
×
453
        }
454

455
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
16✔
456
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
5✔
457
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
5✔
458
                        return ctrl.Result{}, condErr
×
459
                }
×
460

461
                // Clean up stale RenderTasks owned by this target (old versions)
462
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
5✔
463
                for _, ri := range releases {
13✔
464
                        currentRTNames[ri.rtName] = struct{}{}
8✔
465
                }
8✔
466
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
5✔
467
                        log.Error(err, "failed to clean up stale RenderTasks")
×
468
                }
×
469

470
                return ctrl.Result{}, nil
5✔
471
        }
472

473
        // Still running
474
        return ctrl.Result{}, nil
6✔
475
}
476

477
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
146✔
478
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
146✔
479
                Type:               condType,
146✔
480
                Status:             status,
146✔
481
                ObservedGeneration: target.Generation,
146✔
482
                Reason:             reason,
146✔
483
                Message:            message,
146✔
484
        })
146✔
485
        if changed {
191✔
486
                if err := r.Status().Update(ctx, target); err != nil {
48✔
487
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
3✔
488
                }
3✔
489
        }
490

491
        return nil
143✔
492
}
493

494
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
34✔
495
        if len(skipped) == 0 {
59✔
496
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
25✔
497
        }
25✔
498

499
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
500
}
501

502
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
503
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
504
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
505
// It returns the accepted releases and a slice of human-readable filter messages.
506
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
42✔
507
        if len(releases) == 0 {
43✔
508
                return releases, nil
1✔
509
        }
1✔
510

511
        // Step A: uniqueName deduplication.
512
        // When UniqueName is empty, fall back to the parent Component name from the CV.
513
        namedGroups := map[string][]releaseInfo{}
41✔
514

41✔
515
        for _, ri := range releases {
105✔
516
                uniqueName := ri.release.Spec.UniqueName
64✔
517
                if uniqueName == "" {
70✔
518
                        uniqueName = ri.cv.Spec.ComponentRef.Name
6✔
519
                }
6✔
520

521
                namedGroups[uniqueName] = append(namedGroups[uniqueName], ri)
64✔
522
        }
523

524
        var accepted []releaseInfo
41✔
525

41✔
526
        var skipped []string
41✔
527

41✔
528
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
41✔
529
        byPriority := func(a, b releaseInfo) bool {
64✔
530
                if a.release.Spec.Priority != b.release.Spec.Priority {
33✔
531
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
532
                }
10✔
533

534
                return a.bindingKey < b.bindingKey
13✔
535
        }
536

537
        uniqueNames := make([]string, 0, len(namedGroups))
41✔
538
        for k := range namedGroups {
96✔
539
                uniqueNames = append(uniqueNames, k)
55✔
540
        }
55✔
541

542
        sort.Strings(uniqueNames)
41✔
543

41✔
544
        for _, uniqueName := range uniqueNames {
96✔
545
                group := namedGroups[uniqueName]
55✔
546
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
64✔
547

548
                accepted = append(accepted, group[0])
55✔
549

55✔
550
                for _, loser := range group[1:] {
64✔
551
                        skipped = append(skipped, fmt.Sprintf(
9✔
552
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
553
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
554
                        ))
9✔
555
                }
9✔
556
        }
557

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

563
        resolved := make([]releaseInfo, 0, len(accepted))
41✔
564

41✔
565
        for _, ri := range accepted {
96✔
566
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
55✔
567
                var riSelector labels.Selector
55✔
568
                if ri.release.Spec.AntiAffinity != nil {
62✔
569
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
570
                        if err != nil {
8✔
571
                                skipped = append(skipped, fmt.Sprintf(
1✔
572
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
573
                                        ri.bindingKey, err,
1✔
574
                                ))
1✔
575

1✔
576
                                continue
1✔
577
                        }
578

579
                        riSelector = sel
6✔
580
                }
581

582
                // Check both directions: ri's anti-affinity against already-resolved labels,
583
                // and already-resolved anti-affinities against ri's labels.
584
                conflict := ""
54✔
585
                for _, other := range resolved {
68✔
586
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
18✔
587
                                conflict = other.bindingKey
4✔
588
                                break
4✔
589
                        }
590

591
                        if other.release.Spec.AntiAffinity != nil {
11✔
592
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
593
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
594
                                        conflict = other.bindingKey
1✔
595
                                        break
1✔
596
                                }
597
                        }
598
                }
599

600
                if conflict != "" {
59✔
601
                        skipped = append(skipped, fmt.Sprintf(
5✔
602
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
603
                                ri.bindingKey, conflict,
5✔
604
                        ))
5✔
605
                } else {
54✔
606
                        resolved = append(resolved, ri)
49✔
607
                }
49✔
608
        }
609

610
        return resolved, skipped
41✔
611
}
612

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

5✔
619
        rtList := &solarv1alpha1.RenderTaskList{}
5✔
620
        if err := r.List(ctx, rtList,
5✔
621
                client.InNamespace(target.Namespace),
5✔
622
                client.MatchingFields{indexOwnerKind: "Target"},
5✔
623
        ); err != nil {
5✔
624
                return err
×
625
        }
×
626

627
        for i := range rtList.Items {
20✔
628
                rt := &rtList.Items[i]
15✔
629
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
15✔
630
                        continue
×
631
                }
632

633
                if _, current := currentRTNames[rt.Name]; current {
28✔
634
                        continue
13✔
635
                }
636

637
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
2✔
638
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
2✔
639
                        return err
×
640
                }
×
641

642
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
2✔
643
                        "Deleted stale RenderTask %s", rt.Name)
2✔
644
        }
645

646
        return nil
5✔
647
}
648

649
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
650
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
651
        if err := r.List(ctx, rtList,
1✔
652
                client.InNamespace(target.Namespace),
1✔
653
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
654
        ); err != nil {
1✔
655
                return err
×
656
        }
×
657

658
        for i := range rtList.Items {
1✔
659
                rt := &rtList.Items[i]
×
660
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
661
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
662
                                return err
×
663
                        }
×
664
                }
665
        }
666

667
        return nil
1✔
668
}
669

670
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec {
7✔
671
        chartName := fmt.Sprintf("release-%s", rel.Name)
7✔
672
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
673
        tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration())
7✔
674

7✔
675
        var targetNamespace string
7✔
676
        if rel.Spec.TargetNamespace != nil {
14✔
677
                targetNamespace = *rel.Spec.TargetNamespace
7✔
678
        }
7✔
679

680
        return solarv1alpha1.RenderTaskSpec{
7✔
681
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
682
                        Type: solarv1alpha1.RendererConfigTypeRelease,
7✔
683
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
7✔
684
                                Chart: solarv1alpha1.ChartConfig{
7✔
685
                                        Name:        chartName,
7✔
686
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
7✔
687
                                        Version:     tag,
7✔
688
                                        AppVersion:  tag,
7✔
689
                                },
7✔
690
                                Input: solarv1alpha1.ReleaseInput{
7✔
691
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
7✔
692
                                        Resources:  cv.Spec.Resources,
7✔
693
                                        Entrypoint: cv.Spec.Entrypoint,
7✔
694
                                },
7✔
695
                                Values:          rel.Spec.Values,
7✔
696
                                TargetNamespace: targetNamespace,
7✔
697
                        },
7✔
698
                },
7✔
699
                Repository:     repo,
7✔
700
                Tag:            tag,
7✔
701
                BaseURL:        registry.Spec.Hostname,
7✔
702
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
703
                FailedJobTTL:   rel.Spec.FailedJobTTL,
7✔
704
                OwnerName:      target.Name,
7✔
705
                OwnerNamespace: target.Namespace,
7✔
706
                OwnerKind:      "Target",
7✔
707
        }
7✔
708
}
709

710
// buildBootstrapInput constructs the desired BootstrapInput from the current
711
// target and resolved releases. Used for both comparison and spec construction.
712
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
12✔
713
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
12✔
714

12✔
715
        for _, ri := range releases {
31✔
716
                ref, err := ociname.ParseReference(ri.chartURL)
19✔
717
                if err != nil {
19✔
718
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
719
                }
×
720

721
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
19✔
722
                if err != nil {
19✔
723
                        return solarv1alpha1.BootstrapInput{}, err
×
724
                }
×
725

726
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
19✔
727
                        Repository: strings.TrimPrefix(repo, "oci://"),
19✔
728
                        Tag:        ref.Identifier(),
19✔
729
                }
19✔
730
        }
731

732
        return solarv1alpha1.BootstrapInput{
12✔
733
                Releases: resolvedReleases,
12✔
734
                Userdata: target.Spec.Userdata,
12✔
735
        }, nil
12✔
736
}
737

738
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
2✔
739
        input, err := buildBootstrapInput(target, releases)
2✔
740
        if err != nil {
2✔
741
                return solarv1alpha1.RenderTaskSpec{}, err
×
742
        }
×
743

744
        releaseNames := make([]string, 0, len(releases))
2✔
745
        for _, ri := range releases {
5✔
746
                releaseNames = append(releaseNames, ri.name)
3✔
747
        }
3✔
748

749
        sort.Strings(releaseNames)
2✔
750

2✔
751
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
2✔
752
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
2✔
753
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
2✔
754

2✔
755
        return solarv1alpha1.RenderTaskSpec{
2✔
756
                RendererConfig: solarv1alpha1.RendererConfig{
2✔
757
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
2✔
758
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
2✔
759
                                Chart: solarv1alpha1.ChartConfig{
2✔
760
                                        Name:        chartName,
2✔
761
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
2✔
762
                                        Version:     tag,
2✔
763
                                        AppVersion:  tag,
2✔
764
                                },
2✔
765
                                Input: input,
2✔
766
                        },
2✔
767
                },
2✔
768
                Repository:     repo,
2✔
769
                Tag:            tag,
2✔
770
                BaseURL:        registry.Spec.Hostname,
2✔
771
                PushSecretRef:  registry.Spec.SolarSecretRef,
2✔
772
                OwnerName:      target.Name,
2✔
773
                OwnerNamespace: target.Namespace,
2✔
774
                OwnerKind:      "Target",
2✔
775
        }, nil
2✔
776
}
777

778
// SetupWithManager sets up the controller with the Manager.
779
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
780
        return ctrl.NewControllerManagedBy(mgr).
1✔
781
                For(&solarv1alpha1.Target{}).
1✔
782
                Watches(
1✔
783
                        &solarv1alpha1.ReleaseBinding{},
1✔
784
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
785
                ).
1✔
786
                Watches(
1✔
787
                        &solarv1alpha1.RenderTask{},
1✔
788
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
789
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
790
                ).
1✔
791
                Watches(
1✔
792
                        &solarv1alpha1.Registry{},
1✔
793
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
794
                ).
1✔
795
                Watches(
1✔
796
                        &solarv1alpha1.ReferenceGrant{},
1✔
797
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
798
                ).
1✔
799
                Watches(
1✔
800
                        &solarv1alpha1.Release{},
1✔
801
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
802
                ).
1✔
803
                Complete(r)
1✔
804
}
1✔
805

806
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
807
// fromNamespace to reference the named registry.
808
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
809
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
810
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
811
                return false, err
×
812
        }
×
813
        for i := range grantList.Items {
×
814
                grant := &grantList.Items[i]
×
815
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
816
                        return true, nil
×
817
                }
×
818
        }
819

820
        return false, nil
×
821
}
822

823
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
824
// fromNamespace to reference Registry resources in the grant's namespace.
825
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
826
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
827
}
×
828

829
// mapRegistryToTargets maps a Registry event to reconcile requests for all
830
// Targets that reference it — either in the same namespace or cross-namespace.
831
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
10✔
832
        reg, ok := obj.(*solarv1alpha1.Registry)
10✔
833
        if !ok {
10✔
834
                return nil
×
835
        }
×
836

837
        // Same-namespace targets
838
        targetList := &solarv1alpha1.TargetList{}
10✔
839
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
10✔
840
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
841

×
842
                return nil
×
843
        }
×
844

845
        var requests []reconcile.Request
10✔
846
        for _, t := range targetList.Items {
11✔
847
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
1✔
848
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
2✔
849
                        requests = append(requests, reconcile.Request{
1✔
850
                                NamespacedName: types.NamespacedName{
1✔
851
                                        Name:      t.Name,
1✔
852
                                        Namespace: t.Namespace,
1✔
853
                                },
1✔
854
                        })
1✔
855
                }
1✔
856
        }
857

858
        // Cross-namespace targets: find namespaces that have been granted access to
859
        // registries in reg.Namespace, then check their targets.
860
        grantList := &solarv1alpha1.ReferenceGrantList{}
10✔
861
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
10✔
862
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
863
                return requests
×
864
        }
×
865

866
        for i := range grantList.Items {
10✔
867
                grant := &grantList.Items[i]
×
868
                if !grantsRegistryResource(grant) {
×
869
                        continue
×
870
                }
871
                for _, from := range grant.Spec.From {
×
872
                        if from.Kind != "Target" || from.Group != solarGroup {
×
873
                                continue
×
874
                        }
875
                        crossTargets := &solarv1alpha1.TargetList{}
×
876
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
877
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
878
                                continue
×
879
                        }
880
                        for _, t := range crossTargets.Items {
×
881
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
882
                                        requests = append(requests, reconcile.Request{
×
883
                                                NamespacedName: types.NamespacedName{
×
884
                                                        Name:      t.Name,
×
885
                                                        Namespace: t.Namespace,
×
886
                                                },
×
887
                                        })
×
888
                                }
×
889
                        }
890
                }
891
        }
892

893
        return requests
10✔
894
}
895

896
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
897
// either because the grant controls Registry access (Target → Registry) or because
898
// it controls ComponentVersion access (Release → ComponentVersion, Releases live in
899
// the same namespace as their Targets).
900
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
8✔
901
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
8✔
902
        if !ok {
8✔
903
                return nil
×
904
        }
×
905

906
        var requests []reconcile.Request
8✔
907

8✔
908
        if grantsRegistryResource(grant) {
8✔
909
                for _, from := range grant.Spec.From {
×
910
                        if from.Kind != "Target" || from.Group != solarGroup {
×
911
                                continue
×
912
                        }
913
                        targets := &solarv1alpha1.TargetList{}
×
914
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
915
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
916
                                continue
×
917
                        }
918
                        for _, t := range targets.Items {
×
919
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
920
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
921
                                        requests = append(requests, reconcile.Request{
×
922
                                                NamespacedName: types.NamespacedName{
×
923
                                                        Name:      t.Name,
×
924
                                                        Namespace: t.Namespace,
×
925
                                                },
×
926
                                        })
×
927
                                }
×
928
                        }
929
                }
930
        }
931

932
        if grantsComponentVersionResource(grant) {
12✔
933
                for _, from := range grant.Spec.From {
8✔
934
                        if from.Kind != "Release" || from.Group != solarGroup {
4✔
935
                                continue
×
936
                        }
937
                        // Releases and Targets are co-located: list Targets in the Release's namespace.
938
                        targets := &solarv1alpha1.TargetList{}
4✔
939
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
940
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ComponentVersion grant mapping", "namespace", from.Namespace)
×
941
                                continue
×
942
                        }
943
                        for _, t := range targets.Items {
4✔
944
                                requests = append(requests, reconcile.Request{
×
945
                                        NamespacedName: types.NamespacedName{
×
946
                                                Name:      t.Name,
×
947
                                                Namespace: t.Namespace,
×
948
                                        },
×
949
                                })
×
950
                        }
×
951
                }
952
        }
953

954
        return requests
8✔
955
}
956

957
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
958
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
8✔
959
        for _, t := range grant.Spec.To {
16✔
960
                if t.Kind == "Registry" && t.Group == solarGroup {
8✔
961
                        return true
×
962
                }
×
963
        }
964

965
        return false
8✔
966
}
967

968
// mapReleaseToTargets maps a Release event to reconcile requests for all
969
// Targets that are bound to the release via ReleaseBindings.
970
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
63✔
971
        rel, ok := obj.(*solarv1alpha1.Release)
63✔
972
        if !ok {
63✔
973
                return nil
×
974
        }
×
975

976
        bindingList := &solarv1alpha1.ReleaseBindingList{}
63✔
977
        if err := r.List(ctx, bindingList,
63✔
978
                client.InNamespace(rel.Namespace),
63✔
979
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
63✔
980
        ); err != nil {
63✔
981
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
982

×
983
                return nil
×
984
        }
×
985

986
        seen := map[string]struct{}{}
63✔
987
        var requests []reconcile.Request
63✔
988

63✔
989
        for _, rb := range bindingList.Items {
63✔
990
                targetName := rb.Spec.TargetRef.Name
×
991
                if _, ok := seen[targetName]; ok {
×
992
                        continue
×
993
                }
994

995
                seen[targetName] = struct{}{}
×
996
                requests = append(requests, reconcile.Request{
×
997
                        NamespacedName: types.NamespacedName{
×
998
                                Name:      targetName,
×
999
                                Namespace: rb.Namespace,
×
1000
                        },
×
1001
                })
×
1002
        }
1003

1004
        return requests
63✔
1005
}
1006

1007
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
21✔
1008
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
21✔
1009
        if !ok || rb.Spec.TargetRef.Name == "" {
21✔
1010
                return nil
×
1011
        }
×
1012

1013
        return []reconcile.Request{
21✔
1014
                {
21✔
1015
                        NamespacedName: types.NamespacedName{
21✔
1016
                                Name:      rb.Spec.TargetRef.Name,
21✔
1017
                                Namespace: rb.Namespace,
21✔
1018
                        },
21✔
1019
                },
21✔
1020
        }
21✔
1021
}
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