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

opendefensecloud / solution-arsenal / 26638678102

29 May 2026 12:59PM UTC coverage: 72.159% (+0.4%) from 71.738%
26638678102

Pull #554

github

web-flow
Merge 75b9a9244 into ce460a380
Pull Request #554: feat: implemented artifact cleanup

301 of 399 new or added lines in 6 files covered. (75.44%)

4 existing lines in 2 files now uncovered.

2654 of 3678 relevant lines covered (72.16%)

28.66 hits per line

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

68.61
/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
        artifactName        string
56
        artifactBindingName string
57
}
58

59
type TargetReconciler struct {
60
        client.Client
61
        Scheme   *runtime.Scheme
62
        Recorder events.EventRecorder
63
        // WatchNamespace restricts reconciliation to this namespace.
64
        // Should be empty in production (watches all namespaces).
65
        // Intended for use in integration tests only.
66
        WatchNamespace string
67
}
68

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

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

187✔
87
        log.V(1).Info("Target is being reconciled", "req", req)
187✔
88

187✔
89
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
243✔
90
                return ctrl.Result{}, nil
56✔
91
        }
56✔
92

93
        // Fetch target
94
        target := &solarv1alpha1.Target{}
131✔
95
        if err := r.Get(ctx, req.NamespacedName, target); err != nil {
139✔
96
                if apierrors.IsNotFound(err) {
16✔
97
                        return ctrl.Result{}, nil
8✔
98
                }
8✔
99

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

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

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

113
                // Delete owned RenderBindings so the GC controller can clean up orphaned RenderArtifacts.
114
                if err := r.deleteOwnedRenderBindings(ctx, target); err != nil {
2✔
NEW
115
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to delete owned RenderBindings")
×
NEW
116
                }
×
117

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

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

134
                return ctrl.Result{}, nil
2✔
135
        }
136

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

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

150
                return ctrl.Result{}, nil
22✔
151
        }
152

153
        // Resolve render registry — supports cross-namespace via ReferenceGrant
154
        registryNamespace := target.Namespace
99✔
155
        if target.Spec.RenderRegistryNamespace != "" {
99✔
156
                registryNamespace = target.Spec.RenderRegistryNamespace
×
157
        }
×
158

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

172
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
173
                }
174
        }
175

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

187
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
19✔
188
                }
189

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

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

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

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

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

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

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

228
                // Clean up any stale RenderTasks and RenderBindings left from prior reconciles.
229
                if err := r.deleteStaleRenderTasks(ctx, target, map[string]struct{}{}); err != nil {
7✔
NEW
230
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderTasks after all bindings removed")
×
NEW
231
                }
×
232
                if err := r.deleteStaleRenderBindings(ctx, target, map[string]struct{}{}); err != nil {
7✔
NEW
233
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to clean up stale RenderBindings after all bindings removed")
×
NEW
234
                }
×
235

236
                return ctrl.Result{}, nil
7✔
237
        }
238

239
        // For each bound release, ensure a per-release RenderTask exists
240
        var releases []releaseInfo
71✔
241

71✔
242
        pendingDeps := false
71✔
243

71✔
244
        for _, binding := range bindingList.Items {
172✔
245
                rel := &solarv1alpha1.Release{}
101✔
246
                if err := r.Get(ctx, client.ObjectKey{
101✔
247
                        Name:      binding.Spec.ReleaseRef.Name,
101✔
248
                        Namespace: target.Namespace,
101✔
249
                }, rel); err != nil {
101✔
250
                        if apierrors.IsNotFound(err) {
×
251
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
252
                                pendingDeps = true
×
253

×
254
                                continue
×
255
                        }
256

257
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
258
                }
259

260
                cv := &solarv1alpha1.ComponentVersion{}
101✔
261
                cvNamespace := target.Namespace
101✔
262
                if rel.Spec.ComponentVersionNamespace != "" {
101✔
263
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
264
                }
×
265

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

×
281
                                continue
×
282
                        }
283
                }
284

285
                if err := r.Get(ctx, client.ObjectKey{
101✔
286
                        Name:      rel.Spec.ComponentVersionRef.Name,
101✔
287
                        Namespace: cvNamespace,
101✔
288
                }, cv); err != nil {
101✔
289
                        if apierrors.IsNotFound(err) {
×
290
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
291
                                pendingDeps = true
×
292

×
293
                                continue
×
294
                        }
295

296
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
297
                }
298

299
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
101✔
300
                releases = append(releases, releaseInfo{
101✔
301
                        bindingKey: binding.Namespace + "/" + binding.Name,
101✔
302
                        name:       rel.Name,
101✔
303
                        release:    rel,
101✔
304
                        cv:         cv,
101✔
305
                        rtName:     rtName,
101✔
306
                })
101✔
307
        }
308

309
        // Resolve conflicts: deduplicate by uniqueName (priority wins) and apply anti-affinity rules.
310
        var skipped []string
71✔
311
        releases, skipped = resolveReleaseConflicts(releases)
71✔
312
        if condErr := r.setResolvedCondition(ctx, target, skipped); condErr != nil {
71✔
313
                return ctrl.Result{}, condErr
×
314
        }
×
315

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

322
                return ctrl.Result{}, nil
×
323
        }
324

325
        // Create per-release RenderTasks (one per target+release pair).
326
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
327
        allRendered := true
71✔
328

71✔
329
        for i, ri := range releases {
162✔
330
                rt := &solarv1alpha1.RenderTask{}
91✔
331
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
91✔
332

91✔
333
                if apierrors.IsNotFound(err) {
103✔
334
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
12✔
335
                        rt = &solarv1alpha1.RenderTask{
12✔
336
                                ObjectMeta: metav1.ObjectMeta{
12✔
337
                                        Name:      ri.rtName,
12✔
338
                                        Namespace: target.Namespace,
12✔
339
                                },
12✔
340
                                Spec: spec,
12✔
341
                        }
12✔
342

12✔
343
                        if err := r.Create(ctx, rt); err != nil {
12✔
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)
12✔
348
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
12✔
349
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
12✔
350
                } else if err != nil {
79✔
351
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
352
                }
×
353

354
                // Check if release RenderTask is complete
355
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
91✔
356
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
357
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
358
                                return ctrl.Result{}, condErr
×
359
                        }
×
360

361
                        return ctrl.Result{}, nil
×
362
                }
363

364
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
140✔
365
                        releases[i].chartURL = rt.Status.ChartURL
49✔
366

49✔
367
                        // Ensure a RenderArtifact object exists for the pushed OCI artifact, and
49✔
368
                        // create a RenderBinding linking this Target to it.
49✔
369
                        aName := renderArtifactName(target.Namespace, rt.Spec.BaseURL, rt.Spec.Repository, rt.Spec.Tag)
49✔
370
                        bName := renderBindingName(aName, target.Name)
49✔
371
                        // Create the RenderBinding before the RenderArtifact to avoid a race
49✔
372
                        if err := r.ensureRenderBinding(ctx, target, aName, bName); err != nil {
49✔
NEW
373
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for release")
×
NEW
374
                        }
×
375
                        if err := r.ensureRenderArtifact(ctx, aName, rt, registry.Spec.Flavor, registryNamespace); err != nil {
49✔
NEW
376
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for release")
×
NEW
377
                        }
×
378
                        releases[i].artifactName = aName
49✔
379
                        releases[i].artifactBindingName = bName
49✔
380
                } else {
42✔
381
                        allRendered = false
42✔
382
                }
42✔
383
        }
384

385
        if pendingDeps {
71✔
386
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
387
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
388
                        return ctrl.Result{}, condErr
×
389
                }
×
390

391
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
392
        }
393

394
        if !allRendered {
109✔
395
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
38✔
396
                        "Waiting for release RenderTasks to complete"); condErr != nil {
39✔
397
                        return ctrl.Result{}, condErr
1✔
398
                }
1✔
399

400
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
37✔
401
        }
402

403
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
33✔
404
                "All releases rendered successfully"); condErr != nil {
33✔
405
                return ctrl.Result{}, condErr
×
406
        }
×
407

408
        // Determine if a new bootstrap render is needed by checking whether the
409
        // current bootstrapVersion's RenderTask still matches the desired release set.
410
        bootstrapVersion := target.Status.BootstrapVersion
33✔
411
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
33✔
412
        bootstrapRT := &solarv1alpha1.RenderTask{}
33✔
413
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
33✔
414

33✔
415
        needsNewBootstrap := false
33✔
416

33✔
417
        switch {
33✔
418
        case apierrors.IsNotFound(err):
5✔
419
                // No RenderTask for the current version yet — create one
5✔
420
                needsNewBootstrap = true
5✔
421
        case err != nil:
×
422
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
423
        default:
28✔
424
                // RenderTask exists — check if the desired bootstrap input changed
28✔
425
                // (release set, resolved refs/tags, or userdata)
28✔
426
                desiredInput, inputErr := buildBootstrapInput(target, releases)
28✔
427
                if inputErr != nil {
28✔
428
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
429
                }
×
430

431
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
28✔
432
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
30✔
433
                        bootstrapVersion++
2✔
434
                        needsNewBootstrap = true
2✔
435
                }
2✔
436
        }
437

438
        if needsNewBootstrap {
40✔
439
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
7✔
440
                if specErr != nil {
7✔
441
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
442
                }
×
443

444
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
7✔
445
                bootstrapRT = &solarv1alpha1.RenderTask{
7✔
446
                        ObjectMeta: metav1.ObjectMeta{
7✔
447
                                Name:      bootstrapRTName,
7✔
448
                                Namespace: target.Namespace,
7✔
449
                        },
7✔
450
                        Spec: spec,
7✔
451
                }
7✔
452

7✔
453
                if err := r.Create(ctx, bootstrapRT); err != nil {
7✔
454
                        if !apierrors.IsAlreadyExists(err) {
×
455
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
456
                        }
×
457

458
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
×
459
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
460
                        }
×
461
                } else {
7✔
462
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
7✔
463
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
7✔
464
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
7✔
465
                }
7✔
466

467
                // Persist the new bootstrapVersion in status
468
                if bootstrapVersion != target.Status.BootstrapVersion {
9✔
469
                        target.Status.BootstrapVersion = bootstrapVersion
2✔
470
                        if err := r.Status().Update(ctx, target); err != nil {
2✔
471
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
×
472
                        }
×
473
                }
474
        }
475

476
        // Update target status from bootstrap RenderTask
477
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
33✔
478
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
479
                        "Bootstrap rendering failed"); condErr != nil {
×
480
                        return ctrl.Result{}, condErr
×
481
                }
×
482

483
                return ctrl.Result{}, nil
×
484
        }
485

486
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
41✔
487
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
8✔
488
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
8✔
489
                        return ctrl.Result{}, condErr
×
490
                }
×
491

492
                // Ensure RenderArtifact + RenderBinding exist for the bootstrap chart.
493
                bootstrapArtifactName := renderArtifactName(target.Namespace, bootstrapRT.Spec.BaseURL, bootstrapRT.Spec.Repository, bootstrapRT.Spec.Tag)
8✔
494
                bootstrapBindingName := renderBindingName(bootstrapArtifactName, target.Name)
8✔
495
                // Create the RenderBinding before the RenderArtifact to avoid a race
8✔
496
                if err := r.ensureRenderBinding(ctx, target, bootstrapArtifactName, bootstrapBindingName); err != nil {
8✔
NEW
497
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderBinding for bootstrap")
×
NEW
498
                }
×
499
                if err := r.ensureRenderArtifact(ctx, bootstrapArtifactName, bootstrapRT, registry.Spec.Flavor, registryNamespace); err != nil {
8✔
NEW
500
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to ensure RenderArtifact for bootstrap")
×
NEW
501
                }
×
502

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

512
                // Clean up stale RenderBindings owned by this target.
513
                currentBindingNames := map[string]struct{}{bootstrapBindingName: {}}
8✔
514
                for _, ri := range releases {
20✔
515
                        if ri.artifactBindingName != "" {
24✔
516
                                currentBindingNames[ri.artifactBindingName] = struct{}{}
12✔
517
                        }
12✔
518
                }
519
                if err := r.deleteStaleRenderBindings(ctx, target, currentBindingNames); err != nil {
8✔
NEW
520
                        log.Error(err, "failed to clean up stale RenderBindings")
×
NEW
521
                }
×
522

523
                return ctrl.Result{}, nil
8✔
524
        }
525

526
        // Still running
527
        return ctrl.Result{}, nil
25✔
528
}
529

530
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
263✔
531
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
263✔
532
                Type:               condType,
263✔
533
                Status:             status,
263✔
534
                ObservedGeneration: target.Generation,
263✔
535
                Reason:             reason,
263✔
536
                Message:            message,
263✔
537
        })
263✔
538
        if changed {
323✔
539
                if err := r.Status().Update(ctx, target); err != nil {
61✔
540
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
1✔
541
                }
1✔
542
        }
543

544
        return nil
262✔
545
}
546

547
func (r *TargetReconciler) setResolvedCondition(ctx context.Context, target *solarv1alpha1.Target, skipped []string) error {
71✔
548
        if len(skipped) == 0 {
132✔
549
                return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "NoConflicts", "")
61✔
550
        }
61✔
551

552
        return r.setCondition(ctx, target, ConditionTypeReleasesResolved, metav1.ConditionTrue, "Resolved", strings.Join(skipped, "; "))
10✔
553
}
554

555
// resolveReleaseConflicts deduplicates releases by uniqueName (keeping the highest-priority
556
// binding) and filters releases that violate anti-affinity rules of already-accepted releases.
557
// Releases without a uniqueName are deduplicated using the parent Component name from the CV.
558
// It returns the accepted releases and a slice of human-readable filter messages.
559
func resolveReleaseConflicts(releases []releaseInfo) ([]releaseInfo, []string) {
79✔
560
        if len(releases) == 0 {
80✔
561
                return releases, nil
1✔
562
        }
1✔
563

564
        // Step A: uniqueName deduplication.
565
        // When UniqueName is empty, fall back to the parent Component name from the CV.
566
        namedGroups := map[string][]releaseInfo{}
78✔
567

78✔
568
        for _, ri := range releases {
191✔
569
                uniqueName := ri.release.Spec.UniqueName
113✔
570
                if uniqueName == "" {
119✔
571
                        uniqueName = ri.cv.Spec.ComponentRef.Name
6✔
572
                }
6✔
573

574
                namedGroups[uniqueName] = append(namedGroups[uniqueName], ri)
113✔
575
        }
576

577
        var accepted []releaseInfo
78✔
578

78✔
579
        var skipped []string
78✔
580

78✔
581
        // byPriority sorts releases with highest priority first; bindingKey breaks ties.
78✔
582
        byPriority := func(a, b releaseInfo) bool {
113✔
583
                if a.release.Spec.Priority != b.release.Spec.Priority {
45✔
584
                        return a.release.Spec.Priority > b.release.Spec.Priority
10✔
585
                }
10✔
586

587
                return a.bindingKey < b.bindingKey
25✔
588
        }
589

590
        uniqueNames := make([]string, 0, len(namedGroups))
78✔
591
        for k := range namedGroups {
181✔
592
                uniqueNames = append(uniqueNames, k)
103✔
593
        }
103✔
594

595
        sort.Strings(uniqueNames)
78✔
596

78✔
597
        for _, uniqueName := range uniqueNames {
181✔
598
                group := namedGroups[uniqueName]
103✔
599
                sort.Slice(group, func(i, j int) bool { return byPriority(group[i], group[j]) })
113✔
600

601
                accepted = append(accepted, group[0])
103✔
602

103✔
603
                for _, loser := range group[1:] {
113✔
604
                        skipped = append(skipped, fmt.Sprintf(
10✔
605
                                "binding %s filtered: uniqueName %q conflict, lower priority than %s",
10✔
606
                                loser.bindingKey, uniqueName, group[0].bindingKey,
10✔
607
                        ))
10✔
608
                }
10✔
609
        }
610

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

616
        resolved := make([]releaseInfo, 0, len(accepted))
78✔
617

78✔
618
        for _, ri := range accepted {
181✔
619
                // Parse ri's own anti-affinity selector once; bail early on invalid selector.
103✔
620
                var riSelector labels.Selector
103✔
621
                if ri.release.Spec.AntiAffinity != nil {
110✔
622
                        sel, err := metav1.LabelSelectorAsSelector(ri.release.Spec.AntiAffinity)
7✔
623
                        if err != nil {
8✔
624
                                skipped = append(skipped, fmt.Sprintf(
1✔
625
                                        "binding %s filtered: invalid antiAffinity selector: %v",
1✔
626
                                        ri.bindingKey, err,
1✔
627
                                ))
1✔
628

1✔
629
                                continue
1✔
630
                        }
631

632
                        riSelector = sel
6✔
633
                }
634

635
                // Check both directions: ri's anti-affinity against already-resolved labels,
636
                // and already-resolved anti-affinities against ri's labels.
637
                conflict := ""
102✔
638
                for _, other := range resolved {
127✔
639
                        if riSelector != nil && riSelector.Matches(labels.Set(other.release.Labels)) {
29✔
640
                                conflict = other.bindingKey
4✔
641
                                break
4✔
642
                        }
643

644
                        if other.release.Spec.AntiAffinity != nil {
22✔
645
                                otherSel, err := metav1.LabelSelectorAsSelector(other.release.Spec.AntiAffinity)
1✔
646
                                if err == nil && otherSel.Matches(labels.Set(ri.release.Labels)) {
2✔
647
                                        conflict = other.bindingKey
1✔
648
                                        break
1✔
649
                                }
650
                        }
651
                }
652

653
                if conflict != "" {
107✔
654
                        skipped = append(skipped, fmt.Sprintf(
5✔
655
                                "binding %s filtered: anti-affinity conflict with %s",
5✔
656
                                ri.bindingKey, conflict,
5✔
657
                        ))
5✔
658
                } else {
102✔
659
                        resolved = append(resolved, ri)
97✔
660
                }
97✔
661
        }
662

663
        return resolved, skipped
78✔
664
}
665

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

15✔
672
        rtList := &solarv1alpha1.RenderTaskList{}
15✔
673
        if err := r.List(ctx, rtList,
15✔
674
                client.InNamespace(target.Namespace),
15✔
675
                client.MatchingFields{indexOwnerKind: "Target"},
15✔
676
        ); err != nil {
15✔
677
                return err
×
678
        }
×
679

680
        for i := range rtList.Items {
38✔
681
                rt := &rtList.Items[i]
23✔
682
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
23✔
683
                        continue
×
684
                }
685

686
                if _, current := currentRTNames[rt.Name]; current {
43✔
687
                        continue
20✔
688
                }
689

690
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
3✔
691
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
3✔
692
                        return err
×
693
                }
×
694

695
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
3✔
696
                        "Deleted stale RenderTask %s", rt.Name)
3✔
697
        }
698

699
        return nil
15✔
700
}
701

702
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
703
        rtList := &solarv1alpha1.RenderTaskList{}
2✔
704
        if err := r.List(ctx, rtList,
2✔
705
                client.InNamespace(target.Namespace),
2✔
706
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
707
        ); err != nil {
2✔
708
                return err
×
709
        }
×
710

711
        for i := range rtList.Items {
4✔
712
                rt := &rtList.Items[i]
2✔
713
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
4✔
714
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
2✔
715
                                return err
×
716
                        }
×
717
                }
718
        }
719

720
        return nil
2✔
721
}
722

723
// deleteStaleRenderBindings removes RenderBindings owned by this target that are no
724
// longer needed (artifact is not in currentBindingNames).
725
func (r *TargetReconciler) deleteStaleRenderBindings(ctx context.Context, target *solarv1alpha1.Target, currentBindingNames map[string]struct{}) error {
15✔
726
        log := ctrl.LoggerFrom(ctx)
15✔
727

15✔
728
        bindingList := &solarv1alpha1.RenderBindingList{}
15✔
729
        if err := r.List(ctx, bindingList,
15✔
730
                client.InNamespace(target.Namespace),
15✔
731
                client.MatchingFields{indexOwnerKind: "Target"},
15✔
732
        ); err != nil {
15✔
NEW
733
                return err
×
NEW
734
        }
×
735

736
        for i := range bindingList.Items {
38✔
737
                b := &bindingList.Items[i]
23✔
738
                if b.Spec.OwnerName != target.Name || b.Spec.OwnerNamespace != target.Namespace {
23✔
NEW
739
                        continue
×
740
                }
741

742
                if _, current := currentBindingNames[b.Name]; current {
43✔
743
                        continue
20✔
744
                }
745

746
                log.V(1).Info("Deleting stale RenderBinding", "renderBinding", b.Name)
3✔
747
                if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
3✔
NEW
748
                        return err
×
NEW
749
                }
×
750
        }
751

752
        return nil
15✔
753
}
754

755
// deleteOwnedRenderBindings removes all RenderBindings owned by this target.
756
// Called during Target deletion to trigger GC of any associated RenderArtifacts.
757
func (r *TargetReconciler) deleteOwnedRenderBindings(ctx context.Context, target *solarv1alpha1.Target) error {
2✔
758
        bindingList := &solarv1alpha1.RenderBindingList{}
2✔
759
        if err := r.List(ctx, bindingList,
2✔
760
                client.InNamespace(target.Namespace),
2✔
761
                client.MatchingFields{indexOwnerKind: "Target"},
2✔
762
        ); err != nil {
2✔
NEW
763
                return err
×
NEW
764
        }
×
765

766
        for i := range bindingList.Items {
3✔
767
                b := &bindingList.Items[i]
1✔
768
                if b.Spec.OwnerName == target.Name && b.Spec.OwnerNamespace == target.Namespace {
2✔
769
                        if err := r.Delete(ctx, b); client.IgnoreNotFound(err) != nil {
1✔
NEW
770
                                return err
×
NEW
771
                        }
×
772
                }
773
        }
774

775
        return nil
2✔
776
}
777

778
// ensureRenderArtifact creates a RenderArtifact for the given RenderTask's OCI coordinates
779
// if one does not already exist. Idempotent: if it already exists (possibly created by
780
// another Target reconciling the same shared artifact), this is a no-op.
781
func (r *TargetReconciler) ensureRenderArtifact(ctx context.Context, name string, rt *solarv1alpha1.RenderTask, flavor, pushSecretNamespace string) error {
57✔
782
        artifact := &solarv1alpha1.RenderArtifact{}
57✔
783
        if err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: rt.Namespace}, artifact); err == nil {
103✔
784
                return nil
46✔
785
        } else if !apierrors.IsNotFound(err) {
57✔
NEW
786
                return err
×
NEW
787
        }
×
788

789
        artifact = &solarv1alpha1.RenderArtifact{
11✔
790
                ObjectMeta: metav1.ObjectMeta{
11✔
791
                        Name:      name,
11✔
792
                        Namespace: rt.Namespace,
11✔
793
                },
11✔
794
                Spec: solarv1alpha1.RenderArtifactSpec{
11✔
795
                        BaseURL:             rt.Spec.BaseURL,
11✔
796
                        Repository:          rt.Spec.Repository,
11✔
797
                        Tag:                 rt.Spec.Tag,
11✔
798
                        RenderTaskRef:       rt.Name,
11✔
799
                        PushSecretRef:       rt.Spec.PushSecretRef,
11✔
800
                        PushSecretNamespace: pushSecretNamespace,
11✔
801
                        RegistryFlavor:      flavor,
11✔
802
                },
11✔
803
        }
11✔
804

11✔
805
        if err := r.Create(ctx, artifact); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
NEW
806
                return err
×
NEW
807
        }
×
808

809
        return nil
11✔
810
}
811

812
// ensureRenderBinding creates a RenderBinding linking this Target to the named
813
// RenderArtifact if one does not already exist. Idempotent.
814
func (r *TargetReconciler) ensureRenderBinding(ctx context.Context, target *solarv1alpha1.Target, artifactName, bindingName string) error {
57✔
815
        binding := &solarv1alpha1.RenderBinding{}
57✔
816
        if err := r.Get(ctx, client.ObjectKey{Name: bindingName, Namespace: target.Namespace}, binding); err == nil {
103✔
817
                return nil
46✔
818
        } else if !apierrors.IsNotFound(err) {
57✔
NEW
819
                return err
×
NEW
820
        }
×
821

822
        binding = &solarv1alpha1.RenderBinding{
11✔
823
                ObjectMeta: metav1.ObjectMeta{
11✔
824
                        Name:      bindingName,
11✔
825
                        Namespace: target.Namespace,
11✔
826
                },
11✔
827
                Spec: solarv1alpha1.RenderBindingSpec{
11✔
828
                        RenderArtifactRef: corev1.LocalObjectReference{Name: artifactName},
11✔
829
                        OwnerKind:         "Target",
11✔
830
                        OwnerName:         target.Name,
11✔
831
                        OwnerNamespace:    target.Namespace,
11✔
832
                },
11✔
833
        }
11✔
834

11✔
835
        if err := r.Create(ctx, binding); err != nil && !apierrors.IsAlreadyExists(err) {
11✔
NEW
836
                return err
×
NEW
837
        }
×
838

839
        return nil
11✔
840
}
841

842
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec {
12✔
843
        chartName := fmt.Sprintf("release-%s", rel.Name)
12✔
844
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
12✔
845
        tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration())
12✔
846

12✔
847
        var targetNamespace string
12✔
848
        if rel.Spec.TargetNamespace != nil {
24✔
849
                targetNamespace = *rel.Spec.TargetNamespace
12✔
850
        }
12✔
851

852
        return solarv1alpha1.RenderTaskSpec{
12✔
853
                RendererConfig: solarv1alpha1.RendererConfig{
12✔
854
                        Type: solarv1alpha1.RendererConfigTypeRelease,
12✔
855
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
12✔
856
                                Chart: solarv1alpha1.ChartConfig{
12✔
857
                                        Name:        chartName,
12✔
858
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
12✔
859
                                        Version:     tag,
12✔
860
                                        AppVersion:  tag,
12✔
861
                                },
12✔
862
                                Input: solarv1alpha1.ReleaseInput{
12✔
863
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
12✔
864
                                        Resources:  cv.Spec.Resources,
12✔
865
                                        Entrypoint: cv.Spec.Entrypoint,
12✔
866
                                },
12✔
867
                                Values:          rel.Spec.Values,
12✔
868
                                TargetNamespace: targetNamespace,
12✔
869
                        },
12✔
870
                },
12✔
871
                Repository:     repo,
12✔
872
                Tag:            tag,
12✔
873
                BaseURL:        registry.Spec.Hostname,
12✔
874
                PushSecretRef:  registry.Spec.SolarSecretRef,
12✔
875
                FailedJobTTL:   rel.Spec.FailedJobTTL,
12✔
876
                OwnerName:      target.Name,
12✔
877
                OwnerNamespace: target.Namespace,
12✔
878
                OwnerKind:      "Target",
12✔
879
        }
12✔
880
}
881

882
// buildBootstrapInput constructs the desired BootstrapInput from the current
883
// target and resolved releases. Used for both comparison and spec construction.
884
func buildBootstrapInput(target *solarv1alpha1.Target, releases []releaseInfo) (solarv1alpha1.BootstrapInput, error) {
35✔
885
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
35✔
886

35✔
887
        for _, ri := range releases {
83✔
888
                ref, err := ociname.ParseReference(ri.chartURL)
48✔
889
                if err != nil {
48✔
890
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
891
                }
×
892

893
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
48✔
894
                if err != nil {
48✔
895
                        return solarv1alpha1.BootstrapInput{}, err
×
896
                }
×
897

898
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
48✔
899
                        Repository: strings.TrimPrefix(repo, "oci://"),
48✔
900
                        Tag:        ref.Identifier(),
48✔
901
                }
48✔
902
        }
903

904
        return solarv1alpha1.BootstrapInput{
35✔
905
                Releases: resolvedReleases,
35✔
906
                Userdata: target.Spec.Userdata,
35✔
907
        }, nil
35✔
908
}
909

910
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
7✔
911
        input, err := buildBootstrapInput(target, releases)
7✔
912
        if err != nil {
7✔
913
                return solarv1alpha1.RenderTaskSpec{}, err
×
914
        }
×
915

916
        releaseNames := make([]string, 0, len(releases))
7✔
917
        for _, ri := range releases {
16✔
918
                releaseNames = append(releaseNames, ri.name)
9✔
919
        }
9✔
920

921
        sort.Strings(releaseNames)
7✔
922

7✔
923
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
7✔
924
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
7✔
925
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
7✔
926

7✔
927
        return solarv1alpha1.RenderTaskSpec{
7✔
928
                RendererConfig: solarv1alpha1.RendererConfig{
7✔
929
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
7✔
930
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
7✔
931
                                Chart: solarv1alpha1.ChartConfig{
7✔
932
                                        Name:        chartName,
7✔
933
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
7✔
934
                                        Version:     tag,
7✔
935
                                        AppVersion:  tag,
7✔
936
                                },
7✔
937
                                Input: input,
7✔
938
                        },
7✔
939
                },
7✔
940
                Repository:     repo,
7✔
941
                Tag:            tag,
7✔
942
                BaseURL:        registry.Spec.Hostname,
7✔
943
                PushSecretRef:  registry.Spec.SolarSecretRef,
7✔
944
                OwnerName:      target.Name,
7✔
945
                OwnerNamespace: target.Namespace,
7✔
946
                OwnerKind:      "Target",
7✔
947
        }, nil
7✔
948
}
949

950
// SetupWithManager sets up the controller with the Manager.
951
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
952
        return ctrl.NewControllerManagedBy(mgr).
1✔
953
                For(&solarv1alpha1.Target{}).
1✔
954
                Watches(
1✔
955
                        &solarv1alpha1.ReleaseBinding{},
1✔
956
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
957
                ).
1✔
958
                Watches(
1✔
959
                        &solarv1alpha1.RenderTask{},
1✔
960
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
961
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
962
                ).
1✔
963
                Watches(
1✔
964
                        &solarv1alpha1.Registry{},
1✔
965
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
966
                ).
1✔
967
                Watches(
1✔
968
                        &solarv1alpha1.ReferenceGrant{},
1✔
969
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
970
                ).
1✔
971
                Watches(
1✔
972
                        &solarv1alpha1.Release{},
1✔
973
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
974
                ).
1✔
975
                Complete(r)
1✔
976
}
1✔
977

978
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
979
// fromNamespace to reference the named registry.
980
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
981
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
982
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
983
                return false, err
×
984
        }
×
985
        for i := range grantList.Items {
×
986
                grant := &grantList.Items[i]
×
987
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
988
                        return true, nil
×
989
                }
×
990
        }
991

992
        return false, nil
×
993
}
994

995
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
996
// fromNamespace to reference Registry resources in the grant's namespace.
997
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
998
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
999
}
×
1000

1001
// mapRegistryToTargets maps a Registry event to reconcile requests for all
1002
// Targets that reference it — either in the same namespace or cross-namespace.
1003
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
14✔
1004
        reg, ok := obj.(*solarv1alpha1.Registry)
14✔
1005
        if !ok {
14✔
1006
                return nil
×
1007
        }
×
1008

1009
        // Same-namespace targets
1010
        targetList := &solarv1alpha1.TargetList{}
14✔
1011
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
14✔
1012
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
1013

×
1014
                return nil
×
1015
        }
×
1016

1017
        var requests []reconcile.Request
14✔
1018
        for _, t := range targetList.Items {
14✔
1019
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
×
1020
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
×
1021
                        requests = append(requests, reconcile.Request{
×
1022
                                NamespacedName: types.NamespacedName{
×
1023
                                        Name:      t.Name,
×
1024
                                        Namespace: t.Namespace,
×
1025
                                },
×
1026
                        })
×
1027
                }
×
1028
        }
1029

1030
        // Cross-namespace targets: find namespaces that have been granted access to
1031
        // registries in reg.Namespace, then check their targets.
1032
        grantList := &solarv1alpha1.ReferenceGrantList{}
14✔
1033
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
14✔
1034
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
1035
                return requests
×
1036
        }
×
1037

1038
        for i := range grantList.Items {
14✔
1039
                grant := &grantList.Items[i]
×
1040
                if !grantsRegistryResource(grant) {
×
1041
                        continue
×
1042
                }
1043
                for _, from := range grant.Spec.From {
×
1044
                        if from.Kind != "Target" || from.Group != solarGroup {
×
1045
                                continue
×
1046
                        }
1047
                        crossTargets := &solarv1alpha1.TargetList{}
×
1048
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
1049
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
1050
                                continue
×
1051
                        }
1052
                        for _, t := range crossTargets.Items {
×
1053
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
1054
                                        requests = append(requests, reconcile.Request{
×
1055
                                                NamespacedName: types.NamespacedName{
×
1056
                                                        Name:      t.Name,
×
1057
                                                        Namespace: t.Namespace,
×
1058
                                                },
×
1059
                                        })
×
1060
                                }
×
1061
                        }
1062
                }
1063
        }
1064

1065
        return requests
14✔
1066
}
1067

1068
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
1069
// either because the grant controls Registry access (Target → Registry) or because
1070
// it controls ComponentVersion access (Release → ComponentVersion, Releases live in
1071
// the same namespace as their Targets).
1072
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
8✔
1073
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
8✔
1074
        if !ok {
8✔
1075
                return nil
×
1076
        }
×
1077

1078
        var requests []reconcile.Request
8✔
1079

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

1104
        if grantsComponentVersionResource(grant) {
12✔
1105
                for _, from := range grant.Spec.From {
8✔
1106
                        if from.Kind != "Release" || from.Group != solarGroup {
4✔
1107
                                continue
×
1108
                        }
1109
                        // Releases and Targets are co-located: list Targets in the Release's namespace.
1110
                        targets := &solarv1alpha1.TargetList{}
4✔
1111
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
1112
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ComponentVersion grant mapping", "namespace", from.Namespace)
×
1113
                                continue
×
1114
                        }
1115
                        for _, t := range targets.Items {
4✔
1116
                                requests = append(requests, reconcile.Request{
×
1117
                                        NamespacedName: types.NamespacedName{
×
1118
                                                Name:      t.Name,
×
1119
                                                Namespace: t.Namespace,
×
1120
                                        },
×
1121
                                })
×
1122
                        }
×
1123
                }
1124
        }
1125

1126
        return requests
8✔
1127
}
1128

1129
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
1130
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
8✔
1131
        for _, t := range grant.Spec.To {
16✔
1132
                if t.Kind == "Registry" && t.Group == solarGroup {
8✔
1133
                        return true
×
1134
                }
×
1135
        }
1136

1137
        return false
8✔
1138
}
1139

1140
// mapReleaseToTargets maps a Release event to reconcile requests for all
1141
// Targets that are bound to the release via ReleaseBindings.
1142
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
74✔
1143
        rel, ok := obj.(*solarv1alpha1.Release)
74✔
1144
        if !ok {
74✔
1145
                return nil
×
1146
        }
×
1147

1148
        bindingList := &solarv1alpha1.ReleaseBindingList{}
74✔
1149
        if err := r.List(ctx, bindingList,
74✔
1150
                client.InNamespace(rel.Namespace),
74✔
1151
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
74✔
1152
        ); err != nil {
74✔
1153
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
1154

×
1155
                return nil
×
1156
        }
×
1157

1158
        seen := map[string]struct{}{}
74✔
1159
        var requests []reconcile.Request
74✔
1160

74✔
1161
        for _, rb := range bindingList.Items {
74✔
1162
                targetName := rb.Spec.TargetRef.Name
×
1163
                if _, ok := seen[targetName]; ok {
×
1164
                        continue
×
1165
                }
1166

1167
                seen[targetName] = struct{}{}
×
1168
                requests = append(requests, reconcile.Request{
×
1169
                        NamespacedName: types.NamespacedName{
×
1170
                                Name:      targetName,
×
1171
                                Namespace: rb.Namespace,
×
1172
                        },
×
1173
                })
×
1174
        }
1175

1176
        return requests
74✔
1177
}
1178

1179
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
27✔
1180
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
27✔
1181
        if !ok || rb.Spec.TargetRef.Name == "" {
27✔
1182
                return nil
×
1183
        }
×
1184

1185
        return []reconcile.Request{
27✔
1186
                {
27✔
1187
                        NamespacedName: types.NamespacedName{
27✔
1188
                                Name:      rb.Spec.TargetRef.Name,
27✔
1189
                                Namespace: rb.Namespace,
27✔
1190
                        },
27✔
1191
                },
27✔
1192
        }
27✔
1193
}
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