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

opendefensecloud / solution-arsenal / 26507608248

27 May 2026 11:10AM UTC coverage: 72.271%. First build
26507608248

Pull #553

github

web-flow
Merge 4b814daf4 into ce1ad469b
Pull Request #553: feat: improved target secret rendering

119 of 143 new or added lines in 2 files covered. (83.22%)

2463 of 3408 relevant lines covered (72.27%)

22.91 hits per line

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

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

4
package controller
5

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

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

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

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

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

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

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

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

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

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

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

167✔
91
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
222✔
92
                return ctrl.Result{}, nil
55✔
93
        }
55✔
94

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

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

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

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

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

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

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

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

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

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

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

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

169
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
170
                }
171
        }
172

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

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

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

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

196
                return ctrl.Result{}, nil
2✔
197
        }
198

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

204
        // Build hostname→targetPullSecretName lookup from RegistryBindings for this target.
205
        pullSecretsByHost, err := r.buildPullSecretsLookup(ctx, target)
61✔
206
        if err != nil {
71✔
207
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to build pull secrets lookup from RegistryBindings")
10✔
208
        }
10✔
209

210
        // Collect ReleaseBindings for this target
211
        bindingList := &solarv1alpha1.ReleaseBindingList{}
51✔
212
        if err := r.List(ctx, bindingList,
51✔
213
                client.InNamespace(target.Namespace),
51✔
214
                client.MatchingFields{indexReleaseBindingTargetName: target.Name},
51✔
215
        ); err != nil {
51✔
216
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to list ReleaseBindings")
×
217
        }
×
218

219
        if len(bindingList.Items) == 0 {
57✔
220
                log.V(1).Info("No ReleaseBindings found for target")
6✔
221
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoReleaseBindings",
6✔
222
                        "No ReleaseBindings found for this target"); condErr != nil {
6✔
223
                        return ctrl.Result{}, condErr
×
224
                }
×
225

226
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionFalse, "NoReleaseBindings",
6✔
227
                        "No ReleaseBindings found for this target"); condErr != nil {
6✔
228
                        return ctrl.Result{}, condErr
×
229
                }
×
230

231
                return ctrl.Result{}, nil
6✔
232
        }
233

234
        // For each bound release, ensure a per-release RenderTask exists
235
        var releases []releaseInfo
45✔
236

45✔
237
        pendingDeps := false
45✔
238

45✔
239
        for _, binding := range bindingList.Items {
108✔
240
                rel := &solarv1alpha1.Release{}
63✔
241
                if err := r.Get(ctx, client.ObjectKey{
63✔
242
                        Name:      binding.Spec.ReleaseRef.Name,
63✔
243
                        Namespace: target.Namespace,
63✔
244
                }, rel); err != nil {
63✔
245
                        if apierrors.IsNotFound(err) {
×
246
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
247
                                pendingDeps = true
×
248

×
249
                                continue
×
250
                        }
251

252
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
253
                }
254

255
                cv := &solarv1alpha1.ComponentVersion{}
63✔
256
                cvNamespace := target.Namespace
63✔
257
                if rel.Spec.ComponentVersionNamespace != "" {
63✔
258
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
259
                }
×
260

261
                if cvNamespace != target.Namespace {
63✔
262
                        granted := false
×
263
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
264
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
265
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
266
                        }
×
267
                        for i := range grantList.Items {
×
268
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
269
                                        granted = true
×
270
                                }
×
271
                        }
272
                        if !granted {
×
273
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
274
                                pendingDeps = true
×
275

×
276
                                continue
×
277
                        }
278
                }
279

280
                if err := r.Get(ctx, client.ObjectKey{
63✔
281
                        Name:      rel.Spec.ComponentVersionRef.Name,
63✔
282
                        Namespace: cvNamespace,
63✔
283
                }, cv); err != nil {
63✔
284
                        if apierrors.IsNotFound(err) {
×
285
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
286
                                pendingDeps = true
×
287

×
288
                                continue
×
289
                        }
290

291
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
292
                }
293

294
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
63✔
295
                releases = append(releases, releaseInfo{
63✔
296
                        bindingKey: binding.Namespace + "/" + binding.Name,
63✔
297
                        name:       rel.Name,
63✔
298
                        release:    rel,
63✔
299
                        cv:         cv,
63✔
300
                        rtName:     rtName,
63✔
301
                })
63✔
302
        }
303

304
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
305
        var skipped []string
45✔
306
        releases, skipped = resolveReleaseConflicts(releases)
45✔
307
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
45✔
308
                return ctrl.Result{}, condErr
×
309
        }
×
310

311
        if len(releases) == 0 && !pendingDeps {
45✔
312
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "AllReleaseBindingsFiltered",
×
313
                        "All ReleaseBindings were filtered out by the release resolver (uniqueName conflicts or anti-affinity rules)"); condErr != nil {
×
314
                        return ctrl.Result{}, condErr
×
315
                }
×
316

317
                return ctrl.Result{}, nil
×
318
        }
319

320
        // Create per-release RenderTasks (one per target+release pair).
321
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
322
        allRendered := true
45✔
323

45✔
324
        for i, ri := range releases {
99✔
325
                rt := &solarv1alpha1.RenderTask{}
54✔
326
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
54✔
327

54✔
328
                switch {
54✔
329
                case apierrors.IsNotFound(err):
10✔
330
                        spec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
10✔
331
                        if specErr != nil {
10✔
NEW
332
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec")
×
NEW
333
                        }
×
334

335
                        rt = &solarv1alpha1.RenderTask{
10✔
336
                                ObjectMeta: metav1.ObjectMeta{
10✔
337
                                        Name:      ri.rtName,
10✔
338
                                        Namespace: target.Namespace,
10✔
339
                                },
10✔
340
                                Spec: spec,
10✔
341
                        }
10✔
342

10✔
343
                        if err := r.Create(ctx, rt); err != nil {
10✔
344
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
345
                        }
×
346

347
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
10✔
348
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
10✔
349
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
10✔
NEW
350
                case err != nil:
×
351
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
352
                default:
44✔
353
                        // RenderTask exists — check for spec drift (e.g. pull secrets
44✔
354
                        // changed after a RegistryBinding was created/updated).
44✔
355
                        desiredSpec, specErr := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target, pullSecretsByHost)
44✔
356
                        if specErr != nil {
44✔
NEW
357
                                return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute release RenderTask spec for comparison")
×
NEW
358
                        }
×
359

360
                        if !apiequality.Semantic.DeepEqual(rt.Spec, desiredSpec) {
45✔
361
                                if err := r.Delete(ctx, rt); err != nil {
1✔
NEW
362
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete stale release RenderTask")
×
NEW
363
                                }
×
364

365
                                rt = &solarv1alpha1.RenderTask{
1✔
366
                                        ObjectMeta: metav1.ObjectMeta{
1✔
367
                                                Name:      ri.rtName,
1✔
368
                                                Namespace: target.Namespace,
1✔
369
                                        },
1✔
370
                                        Spec: desiredSpec,
1✔
371
                                }
1✔
372

1✔
373
                                if err := r.Create(ctx, rt); err != nil {
1✔
NEW
374
                                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to recreate release RenderTask")
×
NEW
375
                                }
×
376

377
                                log.V(1).Info("Recreated release RenderTask (spec drift)", "release", ri.name, "renderTask", ri.rtName)
1✔
378
                                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Updated", "Update",
1✔
379
                                        "Recreated release RenderTask %s for release %s (spec drift)", ri.rtName, ri.name)
1✔
380
                        }
381
                }
382

383
                // Check if release RenderTask is complete
384
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
54✔
385
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
386
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
387
                                return ctrl.Result{}, condErr
×
388
                        }
×
389

390
                        return ctrl.Result{}, nil
×
391
                }
392

393
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
74✔
394
                        releases[i].chartURL = rt.Status.ChartURL
20✔
395
                } else {
54✔
396
                        allRendered = false
34✔
397
                }
34✔
398
        }
399

400
        if pendingDeps {
45✔
401
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
402
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
403
                        return ctrl.Result{}, condErr
×
404
                }
×
405

406
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
407
        }
408

409
        if !allRendered {
79✔
410
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
34✔
411
                        "Waiting for release RenderTasks to complete"); condErr != nil {
34✔
412
                        return ctrl.Result{}, condErr
×
413
                }
×
414

415
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
34✔
416
        }
417

418
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
11✔
419
                "All releases rendered successfully"); condErr != nil {
11✔
420
                return ctrl.Result{}, condErr
×
421
        }
×
422

423
        // Determine if a new bootstrap render is needed by checking whether the
424
        // current bootstrapVersion's RenderTask still matches the desired release set.
425
        bootstrapVersion := target.Status.BootstrapVersion
11✔
426
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
11✔
427
        bootstrapRT := &solarv1alpha1.RenderTask{}
11✔
428
        err = r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
11✔
429

11✔
430
        needsNewBootstrap := false
11✔
431

11✔
432
        switch {
11✔
433
        case apierrors.IsNotFound(err):
1✔
434
                // No RenderTask for the current version yet — create one
1✔
435
                needsNewBootstrap = true
1✔
436
        case err != nil:
×
437
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
438
        default:
10✔
439
                // RenderTask exists — check if the desired bootstrap input changed
10✔
440
                // (release set, resolved refs/tags, or userdata)
10✔
441
                desiredInput, inputErr := buildBootstrapInput(target, releases, registry.Spec.TargetPullSecretName)
10✔
442
                if inputErr != nil {
10✔
443
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
444
                }
×
445

446
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
10✔
447
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
11✔
448
                        bootstrapVersion++
1✔
449
                        needsNewBootstrap = true
1✔
450
                }
1✔
451
        }
452

453
        if needsNewBootstrap {
13✔
454
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
2✔
455
                if specErr != nil {
2✔
456
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
457
                }
×
458

459
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
2✔
460
                bootstrapRT = &solarv1alpha1.RenderTask{
2✔
461
                        ObjectMeta: metav1.ObjectMeta{
2✔
462
                                Name:      bootstrapRTName,
2✔
463
                                Namespace: target.Namespace,
2✔
464
                        },
2✔
465
                        Spec: spec,
2✔
466
                }
2✔
467

2✔
468
                if err := r.Create(ctx, bootstrapRT); err != nil {
2✔
469
                        if !apierrors.IsAlreadyExists(err) {
×
470
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
471
                        }
×
472

473
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
474
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
475
                        }
×
476
                } else {
2✔
477
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
478
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
479
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
480
                }
2✔
481

482
                // Persist the new bootstrapVersion in status
483
                if bootstrapVersion != target.Status.BootstrapVersion {
3✔
484
                        target.Status.BootstrapVersion = bootstrapVersion
1✔
485
                        if err := r.Status().Update(ctx, target); err != nil {
1✔
486
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
487
                        }
×
488
                }
489
        }
490

491
        // Update target status from bootstrap RenderTask
492
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
11✔
493
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
494
                        "Bootstrap rendering failed"); condErr != nil {
×
495
                        return ctrl.Result{}, condErr
×
496
                }
×
497

498
                return ctrl.Result{}, nil
×
499
        }
500

501
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
16✔
502
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
5✔
503
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
5✔
504
                        return ctrl.Result{}, condErr
×
505
                }
×
506

507
                // Clean up stale RenderTasks owned by this target (old versions)
508
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
5✔
509
                for _, ri := range releases {
13✔
510
                        currentRTNames[ri.rtName] = struct{}{}
8✔
511
                }
8✔
512
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
5✔
513
                        log.Error(err, "failed to clean up stale RenderTasks")
×
514
                }
×
515

516
                return ctrl.Result{}, nil
5✔
517
        }
518

519
        // Still running
520
        return ctrl.Result{}, nil
6✔
521
}
522

523
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
189✔
524
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
189✔
525
                Type:               condType,
189✔
526
                Status:             status,
189✔
527
                ObservedGeneration: target.Generation,
189✔
528
                Reason:             reason,
189✔
529
                Message:            message,
189✔
530
        })
189✔
531
        if changed {
240✔
532
                if err := r.Status().Update(ctx, target); err != nil {
51✔
533
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
×
534
                }
×
535
        }
536

537
        return nil
189✔
538
}
539

540
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
45✔
541
        if len(skipped) == 0 {
81✔
542
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
36✔
543
        }
36✔
544

545
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
9✔
546
}
547

548
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
549
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
550
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
551
// It returns the accepted releases and a slice of human-readable filter messages.
552
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
53✔
553
        if len(releases) == 0 {
54✔
554
                return releases, nil
1✔
555
        }
1✔
556

557
        // Step A: uniqueName deduplication.
558
        // When UniqueName is empty, fall back to the parent Component name from the CV.
559
        namedGroups := map[string][]releaseInfo{}
52✔
560

52✔
561
        for _, ri := range releases {
127✔
562
                uniqueName := ri.release.Spec.UniqueName
75✔
563
                if uniqueName == "" {
81✔
564
                        uniqueName = ri.cv.Spec.ComponentRef.Name
6✔
565
                }
6✔
566

567
                namedGroups[uniqueName] = append(namedGroups[uniqueName], ri)
75✔
568
        }
569

570
        var accepted []releaseInfo
52✔
571

52✔
572
        var skipped []string
52✔
573

52✔
574
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
52✔
575
        byPriority := func(a, b releaseInfo) bool {
75✔
576
                if a.release.Spec.Priority != b.release.Spec.Priority {
33✔
577
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
578
                }
10✔
579

580
                return a.bindingKey < b.bindingKey
13✔
581
        }
582

583
        uniqueNames := make([]string, 0, len(namedGroups))
52✔
584
        for k := range namedGroups {
118✔
585
                uniqueNames = append(uniqueNames, k)
66✔
586
        }
66✔
587

588
        sort.Strings(uniqueNames)
52✔
589

52✔
590
        for _, uniqueName := range uniqueNames {
118✔
591
                group := namedGroups[uniqueName]
66✔
592
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
75✔
593

594
                accepted = append(accepted, group[0])
66✔
595

66✔
596
                for _, loser := range group[1:] {
75✔
597
                        skipped = append(skipped, fmt.Sprintf(
9✔
598
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
9✔
599
                                loser.bindingKey, uniqueName, group[0].bindingKey,
9✔
600
                        ))
9✔
601
                }
9✔
602
        }
603

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

609
        resolved := make([]releaseInfo, 0, len(accepted))
52✔
610

52✔
611
        for _, ri := range accepted {
118✔
612
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
66✔
613
                var riSelector labels.Selector
66✔
614
                if ri.release.Spec.AntiAffinity != nil {
73✔
615
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
616
                        if err != nil {
8✔
617
                                skipped = append(skipped, fmt.Sprintf(
1✔
618
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
619
                                        ri.bindingKey, err,
1✔
620
                                ))
1✔
621

1✔
622
                                continue
1✔
623
                        }
624

625
                        riSelector = sel
6✔
626
                }
627

628
                // Check both directions: ri's anti-affinity against already-resolved labels,
629
                // and already-resolved anti-affinities against ri's labels.
630
                conflict := ""
65✔
631
                for _, other := range resolved {
79✔
632
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
18✔
633
                                conflict = other.bindingKey
4✔
634
                                break
4✔
635
                        }
636

637
                        if other.release.Spec.AntiAffinity != nil {
11✔
638
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
639
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
640
                                        conflict = other.bindingKey
1✔
641
                                        break
1✔
642
                                }
643
                        }
644
                }
645

646
                if conflict != "" {
70✔
647
                        skipped = append(skipped, fmt.Sprintf(
5✔
648
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
649
                                ri.bindingKey, conflict,
5✔
650
                        ))
5✔
651
                } else {
65✔
652
                        resolved = append(resolved, ri)
60✔
653
                }
60✔
654
        }
655

656
        return resolved, skipped
52✔
657
}
658

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

5✔
665
        rtList := &solarv1alpha1.RenderTaskList{}
5✔
666
        if err := r.List(ctx, rtList,
5✔
667
                client.InNamespace(target.Namespace),
5✔
668
                client.MatchingFields{indexOwnerKind: "Target"},
5✔
669
        ); err != nil {
5✔
670
                return err
×
671
        }
×
672

673
        for i := range rtList.Items {
19✔
674
                rt := &rtList.Items[i]
14✔
675
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
14✔
676
                        continue
×
677
                }
678

679
                if _, current := currentRTNames[rt.Name]; current {
27✔
680
                        continue
13✔
681
                }
682

683
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
684
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
685
                        return err
×
686
                }
×
687

688
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
689
                        "Deleted stale RenderTask %s", rt.Name)
1✔
690
        }
691

692
        return nil
5✔
693
}
694

695
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
696
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
697
        if err := r.List(ctx, rtList,
1✔
698
                client.InNamespace(target.Namespace),
1✔
699
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
700
        ); err != nil {
1✔
701
                return err
×
702
        }
×
703

704
        for i := range rtList.Items {
1✔
705
                rt := &rtList.Items[i]
×
706
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
707
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
708
                                return err
×
709
                        }
×
710
                }
711
        }
712

713
        return nil
1✔
714
}
715

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

54✔
720
        var targetNamespace string
54✔
721
        if rel.Spec.TargetNamespace != nil {
108✔
722
                targetNamespace = *rel.Spec.TargetNamespace
54✔
723
        }
54✔
724

725
        resolvedResources, err := resolveResources(cv.Spec.Resources, pullSecretsByHost, r.RegistryBindingStrict)
54✔
726
        if err != nil {
54✔
NEW
727
                return solarv1alpha1.RenderTaskSpec{}, fmt.Errorf("release %s: %w", rel.Name, err)
×
NEW
728
        }
×
729

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

54✔
736
        return solarv1alpha1.RenderTaskSpec{
54✔
737
                RendererConfig: solarv1alpha1.RendererConfig{
54✔
738
                        Type: solarv1alpha1.RendererConfigTypeRelease,
54✔
739
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
54✔
740
                                Chart: solarv1alpha1.ChartConfig{
54✔
741
                                        Name:        chartName,
54✔
742
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
54✔
743
                                        Version:     tag,
54✔
744
                                        AppVersion:  tag,
54✔
745
                                },
54✔
746
                                Input: solarv1alpha1.ReleaseInput{
54✔
747
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
54✔
748
                                        Resources:  resolvedResources,
54✔
749
                                        Entrypoint: cv.Spec.Entrypoint,
54✔
750
                                },
54✔
751
                                Values:          rel.Spec.Values,
54✔
752
                                TargetNamespace: targetNamespace,
54✔
753
                        },
54✔
754
                },
54✔
755
                Repository:     repo,
54✔
756
                Tag:            tag,
54✔
757
                BaseURL:        registry.Spec.Hostname,
54✔
758
                PushSecretRef:  registry.Spec.SolarSecretRef,
54✔
759
                FailedJobTTL:   rel.Spec.FailedJobTTL,
54✔
760
                OwnerName:      target.Name,
54✔
761
                OwnerNamespace: target.Namespace,
54✔
762
                OwnerKind:      "Target",
54✔
763
        }, nil
54✔
764
}
765

766
// resolveResources converts ResourceAccess entries from a ComponentVersion into
767
// ResolvedResourceAccess for the renderer. PullSecretName is looked up from
768
// pullSecretsByHost by extracting the registry host from each resource's repository.
769
// In strict mode, an error is returned if any resource's host has no matching
770
// RegistryBinding.
771
func resolveResources(resources map[string]solarv1alpha1.ResourceAccess, pullSecretsByHost map[string]string, strict bool) (map[string]solarv1alpha1.ResolvedResourceAccess, error) {
61✔
772
        resolved := make(map[string]solarv1alpha1.ResolvedResourceAccess, len(resources))
61✔
773
        for name, ra := range resources {
127✔
774
                host := registryHost(ra.Repository)
66✔
775
                pullSecret, found := pullSecretsByHost[host]
66✔
776
                if strict && !found {
68✔
777
                        return nil, fmt.Errorf("no RegistryBinding for host %q (resource %q); create a RegistryBinding or use relaxed mode", host, name)
2✔
778
                }
2✔
779

780
                resolved[name] = solarv1alpha1.ResolvedResourceAccess{
64✔
781
                        Repository:     ra.Repository,
64✔
782
                        Insecure:       ra.Insecure,
64✔
783
                        Tag:            ra.Tag,
64✔
784
                        Helm:           ra.Helm,
64✔
785
                        PullSecretName: pullSecret,
64✔
786
                }
64✔
787
        }
788

789
        return resolved, nil
59✔
790
}
791

792
// registryHost extracts the registry host from a repository string.
793
// For example, "registry.example.com:5000/foo/bar" returns "registry.example.com:5000".
794
func registryHost(repository string) string {
73✔
795
        repo := strings.TrimPrefix(repository, "oci://")
73✔
796
        if before, _, ok := strings.Cut(repo, "/"); ok {
144✔
797
                return before
71✔
798
        }
71✔
799

800
        return repo
2✔
801
}
802

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

12✔
808
        for _, ri := range releases {
31✔
809
                ref, err := ociname.ParseReference(ri.chartURL)
19✔
810
                if err != nil {
19✔
811
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
812
                }
×
813

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

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

826
        return solarv1alpha1.BootstrapInput{
12✔
827
                Releases: resolvedReleases,
12✔
828
                Userdata: target.Spec.Userdata,
12✔
829
        }, nil
12✔
830
}
831

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

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

843
        sort.Strings(releaseNames)
2✔
844

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

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

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

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

918
        return false, nil
×
919
}
920

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

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

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

×
940
                return nil
×
941
        }
×
942

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

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

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

991
        return requests
18✔
992
}
993

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

1007
        lookup := make(map[string]string, len(rbList.Items))
61✔
1008
        for _, rb := range rbList.Items {
87✔
1009
                reg := &solarv1alpha1.Registry{}
26✔
1010
                if err := r.Get(ctx, client.ObjectKey{
26✔
1011
                        Name:      rb.Spec.RegistryRef.Name,
26✔
1012
                        Namespace: rb.Namespace,
26✔
1013
                }, reg); err != nil {
26✔
NEW
1014
                        return nil, fmt.Errorf("failed to get Registry %s referenced by RegistryBinding %s: %w",
×
NEW
1015
                                rb.Spec.RegistryRef.Name, rb.Name, err)
×
NEW
1016
                }
×
1017

1018
                host := reg.Spec.Hostname
26✔
1019
                if existing, ok := lookup[host]; ok && existing != reg.Spec.TargetPullSecretName {
36✔
1020
                        return nil, fmt.Errorf("conflicting RegistryBindings for host %q: pull secret %q (from %s) vs %q",
10✔
1021
                                host, existing, rb.Name, reg.Spec.TargetPullSecretName)
10✔
1022
                }
10✔
1023

1024
                lookup[host] = reg.Spec.TargetPullSecretName
16✔
1025
        }
1026

1027
        return lookup, nil
51✔
1028
}
1029

1030
// mapRegistryBindingToTarget maps a RegistryBinding event to a reconcile request
1031
// for the referenced Target.
1032
func (r *TargetReconciler) mapRegistryBindingToTarget(ctx context.Context, obj client.Object) []reconcile.Request {
4✔
1033
        rb, ok := obj.(*solarv1alpha1.RegistryBinding)
4✔
1034
        if !ok {
4✔
NEW
1035
                return nil
×
NEW
1036
        }
×
1037

1038
        if rb.Spec.TargetRef.Name == "" {
4✔
NEW
1039
                return nil
×
NEW
1040
        }
×
1041

1042
        return []reconcile.Request{
4✔
1043
                {
4✔
1044
                        NamespacedName: types.NamespacedName{
4✔
1045
                                Name:      rb.Spec.TargetRef.Name,
4✔
1046
                                Namespace: rb.Namespace,
4✔
1047
                        },
4✔
1048
                },
4✔
1049
        }
4✔
1050
}
1051

1052
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1053
// either because the grant controls Registry access (Target → Registry) or because
1054
// it controls ComponentVersion access (Release → ComponentVersion, Releases live in
1055
// the same namespace as their Targets).
1056
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
8✔
1057
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
8✔
1058
        if !ok {
8✔
1059
                return nil
×
1060
        }
×
1061

1062
        var requests []reconcile.Request
8✔
1063

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

1088
        if grantsComponentVersionResource(grant) {
12✔
1089
                for _, from := range grant.Spec.From {
8✔
1090
                        if from.Kind != "Release" || from.Group != solarGroup {
4✔
1091
                                continue
×
1092
                        }
1093
                        // Releases and Targets are co-located: list Targets in the Release's namespace.
1094
                        targets := &solarv1alpha1.TargetList{}
4✔
1095
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
1096
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1097
                                continue
×
1098
                        }
1099
                        for _, t := range targets.Items {
4✔
1100
                                requests = append(requests, reconcile.Request{
×
1101
                                        NamespacedName: types.NamespacedName{
×
1102
                                                Name:      t.Name,
×
1103
                                                Namespace: t.Namespace,
×
1104
                                        },
×
1105
                                })
×
1106
                        }
×
1107
                }
1108
        }
1109

1110
        return requests
8✔
1111
}
1112

1113
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1114
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
8✔
1115
        for _, t := range grant.Spec.To {
16✔
1116
                if t.Kind == "Registry" && t.Group == solarGroup {
8✔
1117
                        return true
×
1118
                }
×
1119
        }
1120

1121
        return false
8✔
1122
}
1123

1124
// mapReleaseToTargets maps a Release event to reconcile requests for all
1125
// Targets that are bound to the release via ReleaseBindings.
1126
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
77✔
1127
        rel, ok := obj.(*solarv1alpha1.Release)
77✔
1128
        if !ok {
77✔
1129
                return nil
×
1130
        }
×
1131

1132
        bindingList := &solarv1alpha1.ReleaseBindingList{}
77✔
1133
        if err := r.List(ctx, bindingList,
77✔
1134
                client.InNamespace(rel.Namespace),
77✔
1135
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
77✔
1136
        ); err != nil {
77✔
1137
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1138

×
1139
                return nil
×
1140
        }
×
1141

1142
        seen := map[string]struct{}{}
77✔
1143
        var requests []reconcile.Request
77✔
1144

77✔
1145
        for _, rb := range bindingList.Items {
78✔
1146
                targetName := rb.Spec.TargetRef.Name
1✔
1147
                if _, ok := seen[targetName]; ok {
1✔
1148
                        continue
×
1149
                }
1150

1151
                seen[targetName] = struct{}{}
1✔
1152
                requests = append(requests, reconcile.Request{
1✔
1153
                        NamespacedName: types.NamespacedName{
1✔
1154
                                Name:      targetName,
1✔
1155
                                Namespace: rb.Namespace,
1✔
1156
                        },
1✔
1157
                })
1✔
1158
        }
1159

1160
        return requests
77✔
1161
}
1162

1163
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
25✔
1164
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
25✔
1165
        if !ok || rb.Spec.TargetRef.Name == "" {
25✔
1166
                return nil
×
1167
        }
×
1168

1169
        return []reconcile.Request{
25✔
1170
                {
25✔
1171
                        NamespacedName: types.NamespacedName{
25✔
1172
                                Name:      rb.Spec.TargetRef.Name,
25✔
1173
                                Namespace: rb.Namespace,
25✔
1174
                        },
25✔
1175
                },
25✔
1176
        }
25✔
1177
}
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