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

opendefensecloud / solution-arsenal / 27414439904

12 Jun 2026 12:03PM UTC coverage: 74.245% (-0.2%) from 74.416%
27414439904

push

github

web-flow
feat: include e2e tests in CI (#575)

## What
Run e2e Tests in CI Pipelines
Closes #481 

## Why
Previously, the E2E test suite had to be triggered manually. To ensure
continuous integration and prevent regressions, these tests are now
automated within the main pipeline.
Additionally, the configuration includes adjustments for local execution
runners (`act`)

## Testing
- Local Verification: Verified the entire E2E suite locally using `act`
with privileged container options and host networking configurations.

## Notes for reviewers
- Local Dev Note: Running the suite locally via `act` now requires an
`.actrc` containing `--container-options "--privileged -v
/var/run/docker.sock:/var/run/docker.sock"` and `--network host`.

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


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

* **New Features**
* CI now exposes a reusable image-tag output and a dedicated end-to-end
test workflow; Makefile supports configurable registry/tag.

* **Tests**
* E2E runs consume produced image tags, create image-pull secrets when
needed, and include a new values fixture; CI vs local behavior is
handled.

* **Bug Fixes**
* Improved checks to avoid noisy grep output and ensure shellcheck is
available before linting.

* **Chores**
* Local/act runs skip signing, SBOM/provenance, QEMU and GHCR login for
faster iteration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

13 of 13 new or added lines in 1 file covered. (100.0%)

13 existing lines in 3 files now uncovered.

2952 of 3976 relevant lines covered (74.25%)

46.17 hits per line

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

72.89
/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
        // uniqueName is the deduplication key computed by resolveReleaseConflicts:
52
        // Spec.UniqueName when set, otherwise the parent Component name from the CV.
53
        // It is guaranteed unique across all surviving releases and used as the
54
        // bootstrap map key to avoid collisions between same-named cross-namespace releases.
55
        uniqueName          string
56
        release             *solarv1alpha1.Release
57
        cv                  *solarv1alpha1.ComponentVersion
58
        rtName              string
59
        chartURL            string
60
        artifactName        string
61
        artifactBindingName string
62
}
63

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

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

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

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

276✔
101
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
366✔
102
                return ctrl.Result{}, nil
90✔
103
        }
90✔
104

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

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

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

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

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

130
                // Remove finalizer
131
                if slices.Contains(target.Finalizers, targetFinalizer) {
4✔
132
                        latest := &solarv1alpha1.Target{}
2✔
133
                        if err := r.Get(ctx, req.NamespacedName, latest); err != nil {
2✔
134
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get latest Target for finalizer removal")
×
135
                        }
×
136

137
                        original := latest.DeepCopy()
2✔
138
                        latest.Finalizers = slices.DeleteFunc(latest.Finalizers, func(s string) bool {
4✔
139
                                return s == targetFinalizer
2✔
140
                        })
2✔
141
                        if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
2✔
142
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to remove finalizer from Target")
×
143
                        }
×
144
                }
145

146
                return ctrl.Result{}, nil
2✔
147
        }
148

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

156
                original := latest.DeepCopy()
34✔
157
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
34✔
158
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
35✔
159
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
1✔
160
                }
1✔
161

162
                return ctrl.Result{}, nil
33✔
163
        }
164

165
        // Resolve render registry — supports cross-namespace via ReferenceGrant
166
        registryNamespace := target.Namespace
146✔
167
        if target.Spec.RenderRegistryNamespace != "" {
146✔
168
                registryNamespace = target.Spec.RenderRegistryNamespace
×
169
        }
×
170

171
        // If the registry lives in a different namespace, verify a ReferenceGrant permits it
172
        // before attempting to fetch the object.
173
        if registryNamespace != target.Namespace {
146✔
174
                granted, err := r.registryGranted(ctx, registryNamespace, target.Namespace)
×
175
                if err != nil {
×
176
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for Registry")
×
177
                }
×
178
                if !granted {
×
179
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotGranted",
×
180
                                "No ReferenceGrant allows access to Registry "+target.Spec.RenderRegistryRef.Name+" in namespace "+registryNamespace); condErr != nil {
×
181
                                return ctrl.Result{}, condErr
×
182
                        }
×
183

184
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
185
                }
186
        }
187

188
        registry := &solarv1alpha1.Registry{}
146✔
189
        if err := r.Get(ctx, client.ObjectKey{
146✔
190
                Name:      target.Spec.RenderRegistryRef.Name,
146✔
191
                Namespace: registryNamespace,
146✔
192
        }, registry); err != nil {
165✔
193
                if apierrors.IsNotFound(err) {
38✔
194
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
19✔
195
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
19✔
UNCOV
196
                                return ctrl.Result{}, condErr
×
UNCOV
197
                        }
×
198

199
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
19✔
200
                }
201

202
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
203
        }
204

205
        if registry.Spec.SolarSecretRef == nil {
129✔
206
                if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "MissingSolarSecretRef",
2✔
207
                        "Registry does not have SolarSecretRef set, required for rendering"); condErr != nil {
2✔
208
                        return ctrl.Result{}, condErr
×
209
                }
×
210

211
                return ctrl.Result{}, nil
2✔
212
        }
213

214
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
125✔
215
                "Registry resolved: "+registry.Name); condErr != nil {
125✔
216
                return ctrl.Result{}, condErr
×
217
        }
×
218

219
        // Build hostname→targetPullSecretName lookup from RegistryBindings for this target.
220
        pullSecretsByHost, err := r.buildPullSecretsLookup(ctx, target)
125✔
221
        if err != nil {
145✔
222
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "RegistryBindingConflict",
20✔
223
                        err.Error()); condErr != nil {
20✔
224
                        return ctrl.Result{}, condErr
×
225
                }
×
226

227
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to build pull secrets lookup from RegistryBindings")
20✔
228
        }
229

230
        // Collect ReleaseBindings for this target — same namespace first, then cross-namespace via ReferenceGrants.
231
        allBindings := &solarv1alpha1.ReleaseBindingList{}
105✔
232
        if err := r.APIReader.List(ctx, allBindings, client.InNamespace(target.Namespace)); err != nil {
105✔
233
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
234
        }
×
235
        bindingList := &solarv1alpha1.ReleaseBindingList{}
105✔
236
        for _, rb := range allBindings.Items {
218✔
237
                if rb.Spec.TargetRef.Name == target.Name && rb.Spec.TargetNamespace == "" {
226✔
238
                        bindingList.Items = append(bindingList.Items, rb)
113✔
239
                }
113✔
240
        }
241

242
        // Collect cross-namespace ReleaseBindings authorized by ReferenceGrants in target's namespace.
243
        crossNsBindings, crossNsErr := r.collectCrossNamespaceReleaseBindings(ctx, target)
105✔
244
        if crossNsErr != nil {
105✔
245
                return ctrl.Result{}, errLogAndWrap(log, crossNsErr, "failed to collect cross-namespace ReleaseBindings")
×
246
        }
×
247
        bindingList.Items = append(bindingList.Items, crossNsBindings...)
105✔
248

105✔
249
        // FIXME: collect cross-namespace RegistryBindings here once ADR-010 is finalized and
105✔
250
        // RegistryBinding collection is wired into the rendering pipeline.
105✔
251

105✔
252
        if len(bindingList.Items) == 0 {
113✔
253
                log.V(1).Info("No ReleaseBindings found for target")
8✔
254
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
8✔
255
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
256
                        return ctrl.Result{}, condErr
×
257
                }
×
258

259
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
8✔
260
                        "No ReleaseBindings found for this target"); condErr != nil {
8✔
261
                        return ctrl.Result{}, condErr
×
262
                }
×
263

264
                // Clean up any stale RenderTasks and RenderBindings left from prior reconciles.
265
                if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
8✔
266
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderTasks after all bindings removed")
×
267
                }
×
268
                if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
8✔
269
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderBindings after all bindings removed")
×
270
                }
×
271

272
                return ctrl.Result{}, nil
8✔
273
        }
274

275
        // For each bound release, ensure a per-release RenderTask exists
276
        var releases []releaseInfo
97✔
277

97✔
278
        pendingDeps := false
97✔
279

97✔
280
        for _, binding := range bindingList.Items {
219✔
281
                rel := &solarv1alpha1.Release{}
122✔
282
                if err := r.Get(ctx, client.ObjectKey{
122✔
283
                        Name:      binding.Spec.ReleaseRef.Name,
122✔
284
                        Namespace: binding.Namespace,
122✔
285
                }, rel); err != nil {
122✔
286
                        if apierrors.IsNotFound(err) {
×
287
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
288
                                pendingDeps = true
×
289

×
290
                                continue
×
291
                        }
292

293
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
294
                }
295

296
                cv := &solarv1alpha1.ComponentVersion{}
122✔
297
                cvNamespace := rel.Namespace
122✔
298
                if rel.Spec.ComponentVersionNamespace != "" {
122✔
299
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
300
                }
×
301

302
                if cvNamespace != rel.Namespace {
122✔
303
                        granted := false
×
304
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
305
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
306
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
307
                        }
×
308
                        for i := range grantList.Items {
×
309
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
310
                                        granted = true
×
311
                                        break
×
312
                                }
313
                        }
314
                        if !granted {
×
315
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
316
                                pendingDeps = true
×
317

×
318
                                continue
×
319
                        }
320
                }
321

322
                if err := r.Get(ctx, client.ObjectKey{
122✔
323
                        Name:      rel.Spec.ComponentVersionRef.Name,
122✔
324
                        Namespace: cvNamespace,
122✔
325
                }, cv); err != nil {
122✔
326
                        if apierrors.IsNotFound(err) {
×
327
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
328
                                pendingDeps = true
×
329

×
330
                                continue
×
331
                        }
332

333
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
334
                }
335

336
                rtName := releaseRenderTaskName(rel.Namespace, rel.Name, target.Name, rel.GetGeneration())
122✔
337
                releases = append(releases, releaseInfo{
122✔
338
                        bindingKey: binding.Namespace + "/" + binding.Name,
122✔
339
                        name:       rel.Name,
122✔
340
                        release:    rel,
122✔
341
                        cv:         cv,
122✔
342
                        rtName:     rtName,
122✔
343
                })
122✔
344
        }
345

346
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
347
        var skipped []string
97✔
348
        releases, skipped = resolveReleaseConflicts(releases)
97✔
349
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
97✔
350
                return ctrl.Result{}, condErr
×
351
        }
×
352

353
        if len(releases) == 0 && !pendingDeps {
97✔
354
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
355
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
356
                        return ctrl.Result{}, condErr
×
357
                }
×
358

359
                return ctrl.Result{}, nil
×
360
        }
361

362
        // Create per-release RenderTasks (one per target+release pair).
363
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
364
        allRendered := true
97✔
365

97✔
366
        for i, ri := range releases {
210✔
367
                rt := &solarv1alpha1.RenderTask{}
113✔
368
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
113✔
369

113✔
370
                switch {
113✔
371
                case apierrors.IsNotFound(err):
29✔
372
                        spec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
29✔
373
                        if specErr != nil {
39✔
374
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
10✔
375
                                        specErr.Error()); condErr != nil {
10✔
376
                                        return ctrl.Result{}, condErr
×
377
                                }
×
378

379
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
10✔
380
                        }
381

382
                        rt = &solarv1alpha1.RenderTask{
19✔
383
                                ObjectMeta: metav1.ObjectMeta{
19✔
384
                                        Name:      ri.rtName,
19✔
385
                                        Namespace: target.Namespace,
19✔
386
                                },
19✔
387
                                Spec: spec,
19✔
388
                        }
19✔
389

19✔
390
                        if err := r.Create(ctx, rt); err != nil {
19✔
391
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
392
                        }
×
393

394
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
19✔
395
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
19✔
396
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
19✔
397
                case err != nil:
×
398
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
399
                default:
84✔
400
                        // RenderTask exists — check for spec drift (e.g. pull secrets
84✔
401
                        // changed after a RegistryBinding was created/updated).
84✔
402
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
84✔
403
                        if specErr != nil {
84✔
404
                                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingRegistryBinding",
×
405
                                        specErr.Error()); condErr != nil {
×
406
                                        return ctrl.Result{}, condErr
×
407
                                }
×
408

409
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
410
                        }
411

412
                        if !apiequality.Semantic.DeepEqual(rt.Spec, desiredSpec) {
86✔
413
                                if err := r.Delete(ctx, rt); err != nil {
2✔
414
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete stale release RenderTask")
×
415
                                }
×
416

417
                                rt = &solarv1alpha1.RenderTask{
2✔
418
                                        ObjectMeta: metav1.ObjectMeta{
2✔
419
                                                Name:      ri.rtName,
2✔
420
                                                Namespace: target.Namespace,
2✔
421
                                        },
2✔
422
                                        Spec: desiredSpec,
2✔
423
                                }
2✔
424

2✔
425
                                if err := r.Create(ctx, rt); err != nil {
2✔
426
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to recreate release RenderTask")
×
427
                                }
×
428

429
                                log.V(1).Info("Recreated release RenderTask (spec drift)", "release", ri.name, "renderTask", ri.rtName)
2✔
430
                                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update",
2✔
431
                                        "Recreated release RenderTask %s for release %s (spec drift)", ri.rtName, ri.name)
2✔
432
                        }
433
                }
434

435
                // Check if release RenderTask is complete
436
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
103✔
437
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
438
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
439
                                return ctrl.Result{}, condErr
×
440
                        }
×
441

442
                        return ctrl.Result{}, nil
×
443
                }
444

445
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
145✔
446
                        releases[i].chartURL = rt.Status.ChartURL
42✔
447

42✔
448
                        // Ensure a RenderArtifact object exists for the pushed OCI artifact, and
42✔
449
                        // create a RenderBinding linking this Target to it.
42✔
450
                        aName := renderArtifactName(target.Namespace, rt.Spec.BaseURL, rt.Spec.Repository, rt.Spec.Tag)
42✔
451
                        bName := renderBindingName(aName, target.Name)
42✔
452
                        // Create the RenderBinding before the RenderArtifact to avoid a race
42✔
453
                        if err := r.ensureRenderBinding(ctx, target, aName, bName); err != nil {
42✔
454
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for release")
×
455
                        }
×
456
                        if err := r.ensureRenderArtifact(ctx, aName, rt, registry.Spec.Flavor, registryNamespace); err != nil {
42✔
457
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for release")
×
458
                        }
×
459
                        releases[i].artifactName = aName
42✔
460
                        releases[i].artifactBindingName = bName
42✔
461
                } else {
61✔
462
                        allRendered = false
61✔
463
                }
61✔
464
        }
465

466
        if pendingDeps {
87✔
467
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
468
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
469
                        return ctrl.Result{}, condErr
×
470
                }
×
471

472
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
473
        }
474

475
        if !allRendered {
145✔
476
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
58✔
477
                        "Waiting for release RenderTasks to complete"); condErr != nil {
59✔
478
                        return ctrl.Result{}, condErr
1✔
479
                }
1✔
480

481
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
57✔
482
        }
483

484
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
29✔
485
                "All releases rendered successfully"); condErr != nil {
29✔
486
                return ctrl.Result{}, condErr
×
487
        }
×
488

489
        // Determine if a new bootstrap render is needed by checking whether the
490
        // current bootstrapVersion's RenderTask still matches the desired release set.
491
        bootstrapVersion := target.Status.BootstrapVersion
29✔
492
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
29✔
493
        bootstrapRT := &solarv1alpha1.RenderTask{}
29✔
494
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
29✔
495

29✔
496
        needsNewBootstrap := false
29✔
497

29✔
498
        switch {
29✔
499
        case apierrors.IsNotFound(err):
5✔
500
                // No RenderTask for the current version yet — create one
5✔
501
                needsNewBootstrap = true
5✔
502
        case err != nil:
×
503
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
504
        default:
24✔
505
                // RenderTask exists — check if the desired bootstrap input changed
24✔
506
                // (release set, resolved refs/tags, or userdata)
24✔
507
                desiredInput, inputErr := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
24✔
508
                if inputErr != nil {
24✔
509
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
510
                }
×
511

512
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
24✔
513
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
27✔
514
                        bootstrapVersion++
3✔
515
                        needsNewBootstrap = true
3✔
516
                }
3✔
517
        }
518

519
        if needsNewBootstrap {
37✔
520
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
8✔
521
                if specErr != nil {
8✔
522
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
523
                }
×
524

525
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
8✔
526
                bootstrapRT = &solarv1alpha1.RenderTask{
8✔
527
                        ObjectMeta: metav1.ObjectMeta{
8✔
528
                                Name:      bootstrapRTName,
8✔
529
                                Namespace: target.Namespace,
8✔
530
                        },
8✔
531
                        Spec: spec,
8✔
532
                }
8✔
533

8✔
534
                if err := r.Create(ctx, bootstrapRT); err != nil {
9✔
535
                        if !apierrors.IsAlreadyExists(err) {
1✔
536
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
537
                        }
×
538

539
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
1✔
540
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
541
                        }
×
542
                } else {
7✔
543
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
7✔
544
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
545
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
7✔
546
                }
7✔
547

548
                // Persist the new bootstrapVersion in status
549
                if bootstrapVersion != target.Status.BootstrapVersion {
11✔
550
                        target.Status.BootstrapVersion = bootstrapVersion
3✔
551
                        if err := r.Status().Update(ctx, target); err != nil {
4✔
552
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
1✔
553
                        }
1✔
554
                }
555
        }
556

557
        // Update target status from bootstrap RenderTask
558
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
28✔
559
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
560
                        "Bootstrap rendering failed"); condErr != nil {
×
561
                        return ctrl.Result{}, condErr
×
562
                }
×
563

564
                return ctrl.Result{}, nil
×
565
        }
566

567
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
36✔
568
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
8✔
569
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
8✔
570
                        return ctrl.Result{}, condErr
×
571
                }
×
572

573
                // Ensure RenderArtifact + RenderBinding exist for the bootstrap chart.
574
                bootstrapArtifactName := renderArtifactName(target.Namespace, bootstrapRT.Spec.BaseURL, bootstrapRT.Spec.Repository, bootstrapRT.Spec.Tag)
8✔
575
                bootstrapBindingName := renderBindingName(bootstrapArtifactName, target.Name)
8✔
576
                // Create the RenderBinding before the RenderArtifact to avoid a race
8✔
577
                if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
8✔
578
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for bootstrap")
×
579
                }
×
580
                if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, registry.Spec.Flavor, registryNamespace); err != nil {
8✔
581
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for bootstrap")
×
582
                }
×
583

584
                // Clean up stale RenderTasks owned by this target (old versions)
585
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
8✔
586
                for _, ri := range releases {
20✔
587
                        currentRTNames[ri.rtName] = struct{}{}
12✔
588
                }
12✔
589
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
8✔
590
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
591
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
592
                        log.Error(err, "failed to clean up stale RenderTasks")
×
593
                }
×
594

595
                // Clean up stale RenderBindings owned by this target.
596
                currentBindingNames := map[string]struct{}{bootstrapBindingName: {}}
8✔
597
                for _, ri := range releases {
20✔
598
                        if ri.artifactBindingName != "" {
24✔
599
                                currentBindingNames[ri.artifactBindingName] = struct{}{}
12✔
600
                        }
12✔
601
                }
602
                if err := r.deleteStaleRenderBindings(ctx, target, currentBindingNames); err != nil {
8✔
603
                        // Stale cleanup is best-effort: a failure here does not affect the desired state
×
604
                        // that was just reconciled. The next reconcile will retry the cleanup.
×
605
                        log.Error(err, "failed to clean up stale RenderBindings")
×
606
                }
×
607

608
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
8✔
609
        }
610

611
        // Still running
612
        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
20✔
613
}
614

615
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
384✔
616
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
384✔
617
                Type:               condType,
384✔
618
                Status:             status,
384✔
619
                ObservedGeneration: target.Generation,
384✔
620
                Reason:             reason,
384✔
621
                Message:            message,
384✔
622
        })
384✔
623
        if changed {
479✔
624
                if err := r.Status().Update(ctx, target); err != nil {
96✔
625
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
1✔
626
                }
1✔
627
        }
628

629
        return nil
383✔
630
}
631

632
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
97✔
633
        if len(skipped) == 0 {
185✔
634
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
88✔
635
        }
88✔
636

637
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
638
}
639

640
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
641
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
642
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
643
// It returns the accepted releases and a slice of human-readable filter messages.
644
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
106✔
645
        if len(releases) == 0 {
107✔
646
                return releases, nil
1✔
647
        }
1✔
648

649
        // Step A: uniqueName deduplication.
650
        // When UniqueName is empty, fall back to the parent Component name from the CV.
651
        namedGroups := map[string][]releaseInfo{}
105✔
652

105✔
653
        for i, ri := range releases {
241✔
654
                uname := effectiveUniqueName(ri.release, ri.cv)
136✔
655
                releases[i].uniqueName = uname
136✔
656
                namedGroups[uname] = append(namedGroups[uname], releases[i])
136✔
657
        }
136✔
658

659
        var accepted []releaseInfo
105✔
660

105✔
661
        var skipped []string
105✔
662

105✔
663
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
105✔
664
        byPriority := func(a, b releaseInfo) bool {
136✔
665
                if a.release.Spec.Priority != b.release.Spec.Priority {
41✔
666
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
667
                }
10✔
668

669
                return a.bindingKey < b.bindingKey
21✔
670
        }
671

672
        uniqueNames := make([]string, 0, len(namedGroups))
105✔
673
        for k := range namedGroups {
232✔
674
                uniqueNames = append(uniqueNames, k)
127✔
675
        }
127✔
676

677
        sort.Strings(uniqueNames)
105✔
678

105✔
679
        for _, uniqueName := range uniqueNames {
232✔
680
                group := namedGroups[uniqueName]
127✔
681
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
136✔
682

683
                accepted = append(accepted, group[0])
127✔
684

127✔
685
                for _, loser := range group[1:] {
136✔
686
                        skipped = append(skipped, fmt.Sprintf(
9✔
687
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
688
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
689
                        ))
9✔
690
                }
9✔
691
        }
692

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

698
        resolved := make([]releaseInfo, 0, len(accepted))
105✔
699

105✔
700
        for _, ri := range accepted {
232✔
701
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
127✔
702
                var riSelector labels.Selector
127✔
703
                if ri.release.Spec.AntiAffinity != nil {
134✔
704
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
705
                        if err != nil {
8✔
706
                                skipped = append(skipped, fmt.Sprintf(
1✔
707
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
708
                                        ri.bindingKey, err,
1✔
709
                                ))
1✔
710

1✔
711
                                continue
1✔
712
                        }
713

714
                        riSelector = sel
6✔
715
                }
716

717
                // Check both directions: ri's anti-affinity against already-resolved labels,
718
                // and already-resolved anti-affinities against ri's labels.
719
                conflict := ""
126✔
720
                for _, other := range resolved {
148✔
721
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
26✔
722
                                conflict = other.bindingKey
4✔
723
                                break
4✔
724
                        }
725

726
                        if other.release.Spec.AntiAffinity != nil {
19✔
727
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
728
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
729
                                        conflict = other.bindingKey
1✔
730
                                        break
1✔
731
                                }
732
                        }
733
                }
734

735
                if conflict != "" {
131✔
736
                        skipped = append(skipped, fmt.Sprintf(
5✔
737
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
738
                                ri.bindingKey, conflict,
5✔
739
                        ))
5✔
740
                } else {
126✔
741
                        resolved = append(resolved, ri)
121✔
742
                }
121✔
743
        }
744

745
        return resolved, skipped
105✔
746
}
747

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

16✔
754
        rtList := &solarv1alpha1.RenderTaskList{}
16✔
755
        if err := r.List(ctx, rtList,
16✔
756
                client.InNamespace(target.Namespace),
16✔
757
                client.MatchingFields{indexOwnerKind: "Target"},
16✔
758
        ); err != nil {
16✔
759
                return err
×
760
        }
×
761

762
        for i := range rtList.Items {
39✔
763
                rt := &rtList.Items[i]
23✔
764
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
23✔
765
                        continue
×
766
                }
767

768
                if _, current := currentRTNames[rt.Name]; current {
43✔
769
                        continue
20✔
770
                }
771

772
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
3✔
773
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
3✔
774
                        return err
×
775
                }
×
776

777
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
3✔
778
                        "Deleted stale RenderTask %s", rt.Name)
3✔
779
        }
780

781
        return nil
16✔
782
}
783

784
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
785
        rtList := &solarv1alpha1.RenderTaskList{}
2✔
786
        if err := r.List(ctx, rtList,
2✔
787
                client.InNamespace(target.Namespace),
2✔
788
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
789
        ); err != nil {
2✔
790
                return err
×
791
        }
×
792

793
        for i := range rtList.Items {
4✔
794
                rt := &rtList.Items[i]
2✔
795
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
4✔
796
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
2✔
797
                                return err
×
798
                        }
×
799
                }
800
        }
801

802
        return nil
2✔
803
}
804

805
// deleteStaleRenderBindings removes RenderBindings owned by this target that are no
806
// longer needed (artifact is not in currentBindingNames).
807
func (r *TargetReconciler) deleteStaleRenderBindings(ctx context.Context, target *solarv1alpha1.Target, currentBindingNames map[string]struct{}) error {
16✔
808
        log := ctrl.LoggerFrom(ctx)
16✔
809

16✔
810
        bindingList := &solarv1alpha1.RenderBindingList{}
16✔
811
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
16✔
UNCOV
812
                return err
×
813
        }
×
814

815
        for i := range bindingList.Items {
39✔
816
                b := &bindingList.Items[i]
23✔
817
                if b.Spec.OwnerKind != "Target" || b.Spec.OwnerName != target.Name || b.Spec.OwnerNamespace != target.Namespace {
23✔
818
                        continue
×
819
                }
820

821
                if _, current := currentBindingNames[b.Name]; current {
43✔
822
                        continue
20✔
823
                }
824

825
                log.V(1).Info("Deleting stale RenderBinding", "renderBinding", b.Name)
3✔
826
                if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
3✔
827
                        return err
×
828
                }
×
829
        }
830

831
        return nil
16✔
832
}
833

834
// deleteOwnedRenderBindings removes all RenderBindings owned by this target.
835
// Called during Target deletion to trigger GC of any associated RenderArtifacts.
836
func (r *TargetReconciler) deleteOwnedRenderBindings(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
837
        bindingList := &solarv1alpha1.RenderBindingList{}
2✔
838
        if err := r.APIReader.List(ctx, bindingList, client.InNamespace(target.Namespace)); err != nil {
2✔
UNCOV
839
                return err
×
840
        }
×
841

842
        for i := range bindingList.Items {
3✔
843
                b := &bindingList.Items[i]
1✔
844
                if b.Spec.OwnerKind == "Target" && b.Spec.OwnerName == target.Name && b.Spec.OwnerNamespace == target.Namespace {
2✔
845
                        if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
1✔
846
                                return err
×
847
                        }
×
848
                }
849
        }
850

851
        return nil
2✔
852
}
853

854
// ensureRenderArtifact creates a RenderArtifact for the given RenderTask's OCI coordinates
855
// if one does not already exist. Idempotent: if it already exists (possibly created by
856
// another Target reconciling the same shared artifact), this is a no-op.
857
//
858
// pushSecretNamespace is passed explicitly because the secret may live in a different
859
// namespace than the RenderTask (e.g. a cluster-scoped secret namespace chosen by the
860
// operator). It must not be inferred from rt.Namespace.
861
func (r *TargetReconciler) ensureRenderArtifact(ctx context.Context, name string, rt *solarv1alpha1.RenderTask, flavor, pushSecretNamespace string) error {
50✔
862
        artifact := &solarv1alpha1.RenderArtifact{}
50✔
863
        if err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: rt.Namespace}, artifact); err == nil {
89✔
864
                if !artifact.DeletionTimestamp.IsZero() {
39✔
865
                        // The artifact is terminating (OCI cleanup in progress). Creating a binding
×
866
                        // against it would race with the finalizer. Requeue and wait for full deletion.
×
867
                        return fmt.Errorf("RenderArtifact %s/%s is terminating; requeuing", rt.Namespace, name)
×
868
                }
×
869

870
                return nil
39✔
871
        } else if !apierrors.IsNotFound(err) {
11✔
872
                return err
×
873
        }
×
874

875
        artifact = &solarv1alpha1.RenderArtifact{
11✔
876
                ObjectMeta: metav1.ObjectMeta{
11✔
877
                        Name:      name,
11✔
878
                        Namespace: rt.Namespace,
11✔
879
                },
11✔
880
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
881
                        BaseURL:             rt.Spec.BaseURL,
11✔
882
                        Repository:          rt.Spec.Repository,
11✔
883
                        Tag:                 rt.Spec.Tag,
11✔
884
                        RenderTaskRef:       rt.Name,
11✔
885
                        PushSecretRef:       rt.Spec.PushSecretRef,
11✔
886
                        PushSecretNamespace: pushSecretNamespace,
11✔
887
                        RegistryFlavor:      flavor,
11✔
888
                },
11✔
889
        }
11✔
890

11✔
891
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
892
                return err
×
893
        }
×
894

895
        return nil
11✔
896
}
897

898
// ensureRenderBinding creates a RenderBinding linking this Target to the named
899
// RenderArtifact if one does not already exist. Idempotent.
900
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string) error {
50✔
901
        binding := &solarv1alpha1.RenderBinding{}
50✔
902
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
89✔
903
                return nil
39✔
904
        } else if !apierrors.IsNotFound(err) {
50✔
905
                return err
×
906
        }
×
907

908
        binding = &solarv1alpha1.RenderBinding{
11✔
909
                ObjectMeta: metav1.ObjectMeta{
11✔
910
                        Name:      bindingName,
11✔
911
                        Namespace: target.Namespace,
11✔
912
                },
11✔
913
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
914
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
915
                        OwnerKind:         "Target",
11✔
916
                        OwnerName:         target.Name,
11✔
917
                        OwnerNamespace:    target.Namespace,
11✔
918
                },
11✔
919
        }
11✔
920

11✔
921
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
922
                return err
×
923
        }
×
924

925
        return nil
11✔
926
}
927

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

113✔
932
        var targetNamespace string
113✔
933
        if rel.Spec.TargetNamespace != nil {
226✔
934
                targetNamespace = *rel.Spec.TargetNamespace
113✔
935
        }
113✔
936

937
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
113✔
938
        if err != nil {
123✔
939
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
10✔
940
        }
10✔
941

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

103✔
948
        return solarv1alpha1.RenderTaskSpec{
103✔
949
                RendererConfig: solarv1alpha1.RendererConfig{
103✔
950
                        Type: solarv1alpha1.RendererConfigTypeRelease,
103✔
951
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
103✔
952
                                Chart: solarv1alpha1.ChartConfig{
103✔
953
                                        Name:        chartName,
103✔
954
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
103✔
955
                                        Version:     tag,
103✔
956
                                        AppVersion:  tag,
103✔
957
                                },
103✔
958
                                Input: solarv1alpha1.ReleaseInput{
103✔
959
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
103✔
960
                                        Resources:  resolvedResources,
103✔
961
                                        Entrypoint: cv.Spec.Entrypoint,
103✔
962
                                },
103✔
963
                                Values:          rel.Spec.Values,
103✔
964
                                TargetNamespace: targetNamespace,
103✔
965
                        },
103✔
966
                },
103✔
967
                Repository:     repo,
103✔
968
                Tag:            tag,
103✔
969
                BaseURL:        registry.Spec.Hostname,
103✔
970
                PushSecretRef:  registry.Spec.SolarSecretRef,
103✔
971
                FailedJobTTL:   rel.Spec.FailedJobTTL,
103✔
972
                OwnerName:      target.Name,
103✔
973
                OwnerNamespace: target.Namespace,
103✔
974
                OwnerKind:      "Target",
103✔
975
        }, nil
103✔
976
}
977

978
// buildBootstrapInput constructs the desired BootstrapInput from the current
979
// target and resolved releases. Used for both comparison and spec construction.
980
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo, renderRegistryPullSecret string) (solarv1alpha1.BootstrapInput, error) {
34✔
981
        resolvedReleases := map[string]solarv1alpha1.ResolvedResourceAccess{}
34✔
982

34✔
983
        for _, ri := range releases {
81✔
984
                if ri.uniqueName == "" {
48✔
985
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("release %q has empty uniqueName; resolveReleaseConflicts must run before buildBootstrapInput", ri.name)
1✔
986
                }
1✔
987

988
                ref, err := ociname.ParseReference(ri.chartURL)
46✔
989
                if err != nil {
46✔
990
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
991
                }
×
992

993
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
46✔
994
                if err != nil {
46✔
995
                        return solarv1alpha1.BootstrapInput{}, err
×
996
                }
×
997

998
                resolvedReleases[ri.uniqueName] = solarv1alpha1.ResolvedResourceAccess{
46✔
999
                        Repository:     strings.TrimPrefix(repo, "oci://"),
46✔
1000
                        Tag:            ref.Identifier(),
46✔
1001
                        PullSecretName: renderRegistryPullSecret,
46✔
1002
                }
46✔
1003
        }
1004

1005
        return solarv1alpha1.BootstrapInput{
33✔
1006
                Releases: resolvedReleases,
33✔
1007
                Userdata: target.Spec.Userdata,
33✔
1008
        }, nil
33✔
1009
}
1010

1011
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
8✔
1012
        input, err := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
8✔
1013
        if err != nil {
8✔
1014
                return solarv1alpha1.RenderTaskSpec{}, err
×
1015
        }
×
1016

1017
        releaseNames := make([]string, 0, len(releases))
8✔
1018
        for _, ri := range releases {
19✔
1019
                releaseNames = append(releaseNames, ri.name)
11✔
1020
        }
11✔
1021

1022
        sort.Strings(releaseNames)
8✔
1023

8✔
1024
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
8✔
1025
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
8✔
1026
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
8✔
1027

8✔
1028
        return solarv1alpha1.RenderTaskSpec{
8✔
1029
                RendererConfig: solarv1alpha1.RendererConfig{
8✔
1030
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
8✔
1031
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
8✔
1032
                                Chart: solarv1alpha1.ChartConfig{
8✔
1033
                                        Name:        chartName,
8✔
1034
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
8✔
1035
                                        Version:     tag,
8✔
1036
                                        AppVersion:  tag,
8✔
1037
                                },
8✔
1038
                                Input: input,
8✔
1039
                        },
8✔
1040
                },
8✔
1041
                Repository:     repo,
8✔
1042
                Tag:            tag,
8✔
1043
                BaseURL:        registry.Spec.Hostname,
8✔
1044
                PushSecretRef:  registry.Spec.SolarSecretRef,
8✔
1045
                OwnerName:      target.Name,
8✔
1046
                OwnerNamespace: target.Namespace,
8✔
1047
                OwnerKind:      "Target",
8✔
1048
        }, nil
8✔
1049
}
1050

1051
// SetupWithManager sets up the controller with the Manager.
1052
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
1053
        return ctrl.NewControllerManagedBy(mgr).
1✔
1054
                For(&solarv1alpha1.Target{}).
1✔
1055
                Watches(
1✔
1056
                        &solarv1alpha1.ReleaseBinding{},
1✔
1057
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
1058
                ).
1✔
1059
                Watches(
1✔
1060
                        &solarv1alpha1.RenderTask{},
1✔
1061
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
1062
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
1063
                ).
1✔
1064
                Watches(
1✔
1065
                        &solarv1alpha1.Registry{},
1✔
1066
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
1067
                ).
1✔
1068
                Watches(
1✔
1069
                        &solarv1alpha1.RegistryBinding{},
1✔
1070
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryBindingToTarget),
1✔
1071
                ).
1✔
1072
                Watches(
1✔
1073
                        &solarv1alpha1.ReferenceGrant{},
1✔
1074
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
1075
                ).
1✔
1076
                Watches(
1✔
1077
                        &solarv1alpha1.Release{},
1✔
1078
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
1079
                ).
1✔
1080
                Complete(r)
1✔
1081
}
1✔
1082

1083
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
1084
// fromNamespace to reference the named registry.
1085
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
1086
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
1087
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
1088
                return false, err
×
1089
        }
×
1090
        for i := range grantList.Items {
×
1091
                grant := &grantList.Items[i]
×
1092
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
1093
                        return true, nil
×
1094
                }
×
1095
        }
1096

1097
        return false, nil
×
1098
}
1099

1100
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
1101
// fromNamespace to reference Registry resources in the grant's namespace.
1102
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
1103
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
1104
}
×
1105

1106
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1107
// Targets that reference it — either in the same namespace or cross-namespace.
1108
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
31✔
1109
        reg, ok := obj.(*solarv1alpha1.Registry)
31✔
1110
        if !ok {
31✔
1111
                return nil
×
1112
        }
×
1113

1114
        // Same-namespace targets
1115
        targetList := &solarv1alpha1.TargetList{}
31✔
1116
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
31✔
1117
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
1118

×
1119
                return nil
×
1120
        }
×
1121

1122
        var requests []reconcile.Request
31✔
1123
        for _, t := range targetList.Items {
32✔
1124
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
1✔
1125
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
1✔
1126
                        requests = append(requests, reconcile.Request{
×
1127
                                NamespacedName: types.NamespacedName{
×
1128
                                        Name:      t.Name,
×
1129
                                        Namespace: t.Namespace,
×
1130
                                },
×
1131
                        })
×
1132
                }
×
1133
        }
1134

1135
        // Cross-namespace targets: find namespaces that have been granted access to
1136
        // registries in reg.Namespace, then check their targets.
1137
        grantList := &solarv1alpha1.ReferenceGrantList{}
31✔
1138
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
31✔
1139
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
1140
                return requests
×
1141
        }
×
1142

1143
        for i := range grantList.Items {
31✔
1144
                grant := &grantList.Items[i]
×
1145
                if !grantsRegistryResource(grant) {
×
1146
                        continue
×
1147
                }
1148
                for _, from := range grant.Spec.From {
×
1149
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1150
                                continue
×
1151
                        }
1152
                        crossTargets := &solarv1alpha1.TargetList{}
×
1153
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
1154
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
1155
                                continue
×
1156
                        }
1157
                        for _, t := range crossTargets.Items {
×
1158
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
1159
                                        requests = append(requests, reconcile.Request{
×
1160
                                                NamespacedName: types.NamespacedName{
×
1161
                                                        Name:      t.Name,
×
1162
                                                        Namespace: t.Namespace,
×
1163
                                                },
×
1164
                                        })
×
1165
                                }
×
1166
                        }
1167
                }
1168
        }
1169

1170
        return requests
31✔
1171
}
1172

1173
// buildPullSecretsLookup lists RegistryBindings for the given target, resolves
1174
// each bound Registry, and returns a map from registry hostname to
1175
// targetPullSecretName. Registries without a targetPullSecretName are included
1176
// with an empty string (anonymous pull).
1177
func (r *TargetReconciler) buildPullSecretsLookup(ctx context.Context, target *solarv1alpha1.Target) (map[string]string, error) {
125✔
1178
        rbList := &solarv1alpha1.RegistryBindingList{}
125✔
1179
        if err := r.List(ctx, rbList,
125✔
1180
                client.InNamespace(target.Namespace),
125✔
1181
                client.MatchingFields{indexRegistryBindingTargetName: target.Name},
125✔
1182
        ); err != nil {
125✔
1183
                return nil, err
×
1184
        }
×
1185

1186
        type hostEntry struct {
125✔
1187
                pullSecret  string
125✔
1188
                bindingName string
125✔
1189
        }
125✔
1190

125✔
1191
        lookup := make(map[string]hostEntry, len(rbList.Items))
125✔
1192

125✔
1193
        for _, rb := range rbList.Items {
163✔
1194
                reg := &solarv1alpha1.Registry{}
38✔
1195
                if err := r.Get(ctx, client.ObjectKey{
38✔
1196
                        Name:      rb.Spec.RegistryRef.Name,
38✔
1197
                        Namespace: rb.Namespace,
38✔
1198
                }, reg); err != nil {
48✔
1199
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
10✔
1200
                                rb.Spec.RegistryRef.Name, rb.Name, err)
10✔
1201
                }
10✔
1202

1203
                host := strings.ToLower(reg.Spec.Hostname)
28✔
1204
                if prev, ok := lookup[host]; ok && prev.pullSecret != reg.Spec.TargetPullSecretName {
38✔
1205
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: RegistryBinding %s (pull secret %q) vs RegistryBinding %s (pull secret %q)",
10✔
1206
                                host, prev.bindingName, prev.pullSecret, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1207
                }
10✔
1208

1209
                lookup[host] = hostEntry{pullSecret: reg.Spec.TargetPullSecretName, bindingName: rb.Name}
18✔
1210
        }
1211

1212
        result := make(map[string]string, len(lookup))
105✔
1213
        for host, entry := range lookup {
113✔
1214
                result[host] = entry.pullSecret
8✔
1215
        }
8✔
1216

1217
        return result, nil
105✔
1218
}
1219

1220
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1221
// for the referenced Target.
1222
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
7✔
1223
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
7✔
1224
        if !ok {
7✔
1225
                return nil
×
1226
        }
×
1227

1228
        if rb.Spec.TargetRef.Name == "" {
7✔
1229
                return nil
×
1230
        }
×
1231

1232
        return []reconcile.Request{
7✔
1233
                {
7✔
1234
                        NamespacedName: types.NamespacedName{
7✔
1235
                                Name:      rb.Spec.TargetRef.Name,
7✔
1236
                                Namespace: rb.Namespace,
7✔
1237
                        },
7✔
1238
                },
7✔
1239
        }
7✔
1240
}
1241

1242
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1243
// either because the grant controls Registry access (Target → Registry) or because
1244
// it controls ComponentVersion access (Release → ComponentVersion).
1245
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
13✔
1246
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
13✔
1247
        if !ok {
13✔
1248
                return nil
×
1249
        }
×
1250

1251
        var requests []reconcile.Request
13✔
1252

13✔
1253
        if grantsRegistryResource(grant) {
13✔
1254
                for _, from := range grant.Spec.From {
×
1255
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1256
                                continue
×
1257
                        }
1258
                        targets := &solarv1alpha1.TargetList{}
×
1259
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
1260
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
1261
                                continue
×
1262
                        }
1263
                        for _, t := range targets.Items {
×
1264
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
1265
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
1266
                                        requests = append(requests, reconcile.Request{
×
1267
                                                NamespacedName: types.NamespacedName{
×
1268
                                                        Name:      t.Name,
×
1269
                                                        Namespace: t.Namespace,
×
1270
                                                },
×
1271
                                        })
×
1272
                                }
×
1273
                        }
1274
                }
1275
        }
1276

1277
        if grantsComponentVersionResource(grant) {
18✔
1278
                seen := map[string]struct{}{}
5✔
1279
                for _, from := range grant.Spec.From {
10✔
1280
                        if from.Kind != "Release" || from.Group != solarGroup {
5✔
1281
                                continue
×
1282
                        }
1283
                        bindings := &solarv1alpha1.ReleaseBindingList{}
5✔
1284
                        if err := r.List(ctx, bindings, client.InNamespace(from.Namespace)); err != nil {
5✔
1285
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1286
                                continue
×
1287
                        }
1288
                        for _, rb := range bindings.Items {
6✔
1289
                                if rb.Spec.TargetRef.Name == "" {
1✔
1290
                                        continue
×
1291
                                }
1292
                                targetNs := rb.Namespace
1✔
1293
                                if rb.Spec.TargetNamespace != "" {
2✔
1294
                                        targetNs = rb.Spec.TargetNamespace
1✔
1295
                                }
1✔
1296
                                key := targetNs + "/" + rb.Spec.TargetRef.Name
1✔
1297
                                if _, ok := seen[key]; ok {
1✔
1298
                                        continue
×
1299
                                }
1300
                                seen[key] = struct{}{}
1✔
1301
                                requests = append(requests, reconcile.Request{
1✔
1302
                                        NamespacedName: types.NamespacedName{
1✔
1303
                                                Name:      rb.Spec.TargetRef.Name,
1✔
1304
                                                Namespace: targetNs,
1✔
1305
                                        },
1✔
1306
                                })
1✔
1307
                        }
1308
                }
1309
        }
1310

1311
        if grantsReleaseBindingToTargetResource(grant) {
17✔
1312
                // The grant lives in the Target's namespace and authorizes ReleaseBindings from
4✔
1313
                // other namespaces. Enqueue all Targets in the grant's namespace so they pick up
4✔
1314
                // the new or removed cross-namespace ReleaseBindings.
4✔
1315
                targets := &solarv1alpha1.TargetList{}
4✔
1316
                if err := r.List(ctx, targets, client.InNamespace(grant.Namespace)); err != nil {
4✔
1317
                        ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReleaseBinding grant mapping", "namespace", grant.Namespace)
×
1318
                } else {
4✔
1319
                        for _, t := range targets.Items {
8✔
1320
                                requests = append(requests, reconcile.Request{
4✔
1321
                                        NamespacedName: types.NamespacedName{
4✔
1322
                                                Name:      t.Name,
4✔
1323
                                                Namespace: t.Namespace,
4✔
1324
                                        },
4✔
1325
                                })
4✔
1326
                        }
4✔
1327
                }
1328
        }
1329

1330
        return requests
13✔
1331
}
1332

1333
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1334
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
13✔
1335
        for _, t := range grant.Spec.To {
26✔
1336
                if t.Kind == "Registry" && t.Group == solarGroup {
13✔
1337
                        return true
×
1338
                }
×
1339
        }
1340

1341
        return false
13✔
1342
}
1343

1344
// grantsReleaseBindingToTargetResource returns true if the ReferenceGrant authorizes
1345
// ReleaseBindings in another namespace to reference Targets in the grant's namespace.
1346
func grantsReleaseBindingToTargetResource(grant *solarv1alpha1.ReferenceGrant) bool {
25✔
1347
        hasReleaseBindingFrom := false
25✔
1348
        for _, f := range grant.Spec.From {
50✔
1349
                if f.Kind == "ReleaseBinding" && f.Group == solarGroup {
41✔
1350
                        hasReleaseBindingFrom = true
16✔
1351
                        break
16✔
1352
                }
1353
        }
1354
        if !hasReleaseBindingFrom {
34✔
1355
                return false
9✔
1356
        }
9✔
1357
        for _, t := range grant.Spec.To {
32✔
1358
                if t.Kind == "Target" && t.Group == solarGroup {
32✔
1359
                        return true
16✔
1360
                }
16✔
1361
        }
1362

1363
        return false
×
1364
}
1365

1366
// collectCrossNamespaceReleaseBindings returns ReleaseBindings from other namespaces
1367
// that reference target via spec.targetRef.name + spec.targetNamespace, authorized by
1368
// a ReferenceGrant in target's namespace.
1369
func (r *TargetReconciler) collectCrossNamespaceReleaseBindings(ctx context.Context, target *solarv1alpha1.Target) ([]solarv1alpha1.ReleaseBinding, error) {
105✔
1370
        grantList := &solarv1alpha1.ReferenceGrantList{}
105✔
1371
        if err := r.List(ctx, grantList, client.InNamespace(target.Namespace)); err != nil {
105✔
1372
                return nil, err
×
1373
        }
×
1374

1375
        seen := make(map[string]struct{})
105✔
1376
        var result []solarv1alpha1.ReleaseBinding
105✔
1377
        for i := range grantList.Items {
117✔
1378
                grant := &grantList.Items[i]
12✔
1379
                if !grantsReleaseBindingToTargetResource(grant) {
12✔
1380
                        continue
×
1381
                }
1382
                for _, from := range grant.Spec.From {
24✔
1383
                        if from.Kind != "ReleaseBinding" || from.Group != solarGroup {
12✔
1384
                                continue
×
1385
                        }
1386
                        crossBindings := &solarv1alpha1.ReleaseBindingList{}
12✔
1387
                        if err := r.List(ctx, crossBindings,
12✔
1388
                                client.InNamespace(from.Namespace),
12✔
1389
                                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
12✔
1390
                        ); err != nil {
12✔
1391
                                return nil, err
×
1392
                        }
×
1393
                        for _, rb := range crossBindings.Items {
24✔
1394
                                if rb.Spec.TargetNamespace != target.Namespace {
12✔
1395
                                        continue
×
1396
                                }
1397
                                key := rb.Namespace + "/" + rb.Name
12✔
1398
                                if _, exists := seen[key]; exists {
15✔
1399
                                        continue
3✔
1400
                                }
1401
                                seen[key] = struct{}{}
9✔
1402
                                result = append(result, rb)
9✔
1403
                        }
1404
                }
1405
        }
1406

1407
        return result, nil
105✔
1408
}
1409

1410
// mapReleaseToTargets maps a Release event to reconcile requests for all
1411
// Targets that are bound to the release via ReleaseBindings.
1412
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
105✔
1413
        rel, ok := obj.(*solarv1alpha1.Release)
105✔
1414
        if !ok {
105✔
1415
                return nil
×
1416
        }
×
1417

1418
        bindingList := &solarv1alpha1.ReleaseBindingList{}
105✔
1419
        if err := r.List(ctx, bindingList,
105✔
1420
                client.InNamespace(rel.Namespace),
105✔
1421
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
105✔
1422
        ); err != nil {
105✔
1423
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1424

×
1425
                return nil
×
1426
        }
×
1427

1428
        seen := map[string]struct{}{}
105✔
1429
        var requests []reconcile.Request
105✔
1430

105✔
1431
        for _, rb := range bindingList.Items {
107✔
1432
                targetNs := rb.Namespace
2✔
1433
                if rb.Spec.TargetNamespace != "" {
2✔
1434
                        targetNs = rb.Spec.TargetNamespace
×
1435
                }
×
1436

1437
                key := targetNs + "/" + rb.Spec.TargetRef.Name
2✔
1438
                if _, ok := seen[key]; ok {
2✔
1439
                        continue
×
1440
                }
1441

1442
                seen[key] = struct{}{}
2✔
1443
                requests = append(requests, reconcile.Request{
2✔
1444
                        NamespacedName: types.NamespacedName{
2✔
1445
                                Name:      rb.Spec.TargetRef.Name,
2✔
1446
                                Namespace: targetNs,
2✔
1447
                        },
2✔
1448
                })
2✔
1449
        }
1450

1451
        return requests
105✔
1452
}
1453

1454
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
39✔
1455
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
39✔
1456
        if !ok || rb.Spec.TargetRef.Name == "" {
39✔
1457
                return nil
×
1458
        }
×
1459

1460
        targetNs := rb.Namespace
39✔
1461
        if rb.Spec.TargetNamespace != "" {
48✔
1462
                targetNs = rb.Spec.TargetNamespace
9✔
1463
        }
9✔
1464

1465
        return []reconcile.Request{
39✔
1466
                {
39✔
1467
                        NamespacedName: types.NamespacedName{
39✔
1468
                                Name:      rb.Spec.TargetRef.Name,
39✔
1469
                                Namespace: targetNs,
39✔
1470
                        },
39✔
1471
                },
39✔
1472
        }
39✔
1473
}
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