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

opendefensecloud / solution-arsenal / 25484408909

07 May 2026 08:19AM UTC coverage: 70.732% (+0.5%) from 70.231%
25484408909

push

github

web-flow
fix: update componentversion version in e2e test (#506)

## What
Fixed failing e2e test for cross ns ComponentVersions after demo chart
was updated



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

## Summary by CodeRabbit

* **Tests**
* Updated test configuration reference for cross-namespace component
testing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

2262 of 3198 relevant lines covered (70.73%)

28.01 hits per line

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

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

4
package controller
5

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

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

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

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

37
        ConditionTypeRegistryResolved = "RegistryResolved"
38
        ConditionTypeReleasesRendered = "ReleasesRendered"
39
        ConditionTypeBootstrapReady   = "BootstrapReady"
40
)
41

42
var ErrReleaseNotRenderedYet = errors.New("release is not rendered yet")
43

44
type releaseInfo struct {
45
        name     string
46
        release  *solarv1alpha1.Release
47
        cv       *solarv1alpha1.ComponentVersion
48
        rtName   string
49
        chartURL string
50
}
51

52
type TargetReconciler struct {
53
        client.Client
54
        Scheme   *runtime.Scheme
55
        Recorder events.EventRecorder
56
        // WatchNamespace restricts reconciliation to this namespace.
57
        // Should be empty in production (watches all namespaces).
58
        // Intended for use in integration tests only.
59
        WatchNamespace string
60
}
61

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

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

97✔
79
        log.V(1).Info("Target is being reconciled", "req", req)
97✔
80

97✔
81
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
129✔
82
                return ctrl.Result{}, nil
32✔
83
        }
32✔
84

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

92
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get object")
×
93
        }
94

95
        // Handle deletion
96
        if !target.DeletionTimestamp.IsZero() {
59✔
97
                log.V(1).Info("Target is being deleted")
1✔
98
                r.Recorder.Eventf(target, nil, corev1.EventTypeWarning, "Deleting", "Reconcile", "Target is being deleted, cleaning up RenderTasks")
1✔
99

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

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

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

121
                return ctrl.Result{}, nil
1✔
122
        }
123

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

131
                original := latest.DeepCopy()
15✔
132
                latest.Finalizers = append(latest.Finalizers, targetFinalizer)
15✔
133
                if err := r.Patch(ctx, latest, client.MergeFrom(original)); err != nil {
15✔
134
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to add finalizer to Target")
×
135
                }
×
136

137
                return ctrl.Result{}, nil
15✔
138
        }
139

140
        // Resolve render registry — supports cross-namespace via ReferenceGrant
141
        registryNamespace := target.Namespace
42✔
142
        if target.Spec.RenderRegistryNamespace != "" {
42✔
143
                registryNamespace = target.Spec.RenderRegistryNamespace
×
144
        }
×
145

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

159
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
160
                }
161
        }
162

163
        registry := &solarv1alpha1.Registry{}
42✔
164
        if err := r.Get(ctx, client.ObjectKey{
42✔
165
                Name:      target.Spec.RenderRegistryRef.Name,
42✔
166
                Namespace: registryNamespace,
42✔
167
        }, registry); err != nil {
61✔
168
                if apierrors.IsNotFound(err) {
38✔
169
                        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionFalse, "NotFound",
19✔
170
                                "Registry not found: "+target.Spec.RenderRegistryRef.Name); condErr != nil {
20✔
171
                                return ctrl.Result{}, condErr
1✔
172
                        }
1✔
173

174
                        return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
18✔
175
                }
176

177
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Registry")
×
178
        }
179

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

186
                return ctrl.Result{}, nil
2✔
187
        }
188

189
        if condErr := r.setCondition(ctx, target, ConditionTypeRegistryResolved, metav1.ConditionTrue, "Resolved",
21✔
190
                "Registry resolved: "+registry.Name); condErr != nil {
21✔
191
                return ctrl.Result{}, condErr
×
192
        }
×
193

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

203
        if len(bindingList.Items) == 0 {
26✔
204
                log.V(1).Info("No ReleaseBindings found for target")
5✔
205
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "NoBindings",
5✔
206
                        "No ReleaseBindings found for this target"); condErr != nil {
5✔
207
                        return ctrl.Result{}, condErr
×
208
                }
×
209

210
                return ctrl.Result{}, nil
5✔
211
        }
212

213
        // For each bound release, ensure a per-release RenderTask exists
214
        var releases []releaseInfo
16✔
215

16✔
216
        pendingDeps := false
16✔
217

16✔
218
        for _, binding := range bindingList.Items {
38✔
219
                rel := &solarv1alpha1.Release{}
22✔
220
                if err := r.Get(ctx, client.ObjectKey{
22✔
221
                        Name:      binding.Spec.ReleaseRef.Name,
22✔
222
                        Namespace: target.Namespace,
22✔
223
                }, rel); err != nil {
22✔
224
                        if apierrors.IsNotFound(err) {
×
225
                                log.V(1).Info("Release not found", "release", binding.Spec.ReleaseRef.Name)
×
226
                                pendingDeps = true
×
227

×
228
                                continue
×
229
                        }
230

231
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get Release")
×
232
                }
233

234
                cv := &solarv1alpha1.ComponentVersion{}
22✔
235
                cvNamespace := target.Namespace
22✔
236
                if rel.Spec.ComponentVersionNamespace != "" {
22✔
237
                        cvNamespace = rel.Spec.ComponentVersionNamespace
×
238
                }
×
239

240
                if cvNamespace != target.Namespace {
22✔
241
                        granted := false
×
242
                        grantList := &solarv1alpha1.ReferenceGrantList{}
×
243
                        if err := r.List(ctx, grantList, client.InNamespace(cvNamespace)); err != nil {
×
244
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to check ReferenceGrant for cross-namespace ComponentVersion")
×
245
                        }
×
246
                        for i := range grantList.Items {
×
247
                                if grantPermitsComponentVersionAccess(&grantList.Items[i], rel.Namespace) {
×
248
                                        granted = true
×
249
                                }
×
250
                        }
251
                        if !granted {
×
252
                                log.V(1).Info("ComponentVersion access not granted", "cv", rel.Spec.ComponentVersionRef.Name, "namespace", cvNamespace)
×
253
                                pendingDeps = true
×
254

×
255
                                continue
×
256
                        }
257
                }
258

259
                if err := r.Get(ctx, client.ObjectKey{
22✔
260
                        Name:      rel.Spec.ComponentVersionRef.Name,
22✔
261
                        Namespace: cvNamespace,
22✔
262
                }, cv); err != nil {
22✔
263
                        if apierrors.IsNotFound(err) {
×
264
                                log.V(1).Info("ComponentVersion not found", "cv", rel.Spec.ComponentVersionRef.Name)
×
265
                                pendingDeps = true
×
266

×
267
                                continue
×
268
                        }
269

270
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get ComponentVersion")
×
271
                }
272

273
                rtName := releaseRenderTaskName(rel.Name, target.Name, rel.GetGeneration())
22✔
274
                releases = append(releases, releaseInfo{
22✔
275
                        name:    rel.Name,
22✔
276
                        release: rel,
22✔
277
                        cv:      cv,
22✔
278
                        rtName:  rtName,
22✔
279
                })
22✔
280
        }
281

282
        // Create per-release RenderTasks (one per target+release pair).
283
        // The renderer job handles dedup by skipping if the chart already exists in the registry.
284
        allRendered := true
16✔
285

16✔
286
        for i, ri := range releases {
38✔
287
                rt := &solarv1alpha1.RenderTask{}
22✔
288
                err := r.Get(ctx, client.ObjectKey{Name: ri.rtName, Namespace: target.Namespace}, rt)
22✔
289

22✔
290
                if apierrors.IsNotFound(err) {
25✔
291
                        spec := r.computeReleaseRenderTaskSpec(ri.release, ri.cv, registry, target)
3✔
292
                        rt = &solarv1alpha1.RenderTask{
3✔
293
                                ObjectMeta: metav1.ObjectMeta{
3✔
294
                                        Name:      ri.rtName,
3✔
295
                                        Namespace: target.Namespace,
3✔
296
                                },
3✔
297
                                Spec: spec,
3✔
298
                        }
3✔
299

3✔
300
                        if err := r.Create(ctx, rt); err != nil {
3✔
301
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create release RenderTask")
×
302
                        }
×
303

304
                        log.V(1).Info("Created release RenderTask", "release", ri.name, "renderTask", ri.rtName)
3✔
305
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
3✔
306
                                "Created release RenderTask %s for release %s", ri.rtName, ri.name)
3✔
307
                } else if err != nil {
19✔
308
                        return ctrl.Result{}, errLogAndWrap(log, err, "failed to get release RenderTask")
×
309
                }
×
310

311
                // Check if release RenderTask is complete
312
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
22✔
313
                        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "ReleaseFailed",
×
314
                                fmt.Sprintf("Release %s rendering failed", ri.name)); condErr != nil {
×
315
                                return ctrl.Result{}, condErr
×
316
                        }
×
317

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

321
                if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) && rt.Status.ChartURL != "" {
38✔
322
                        releases[i].chartURL = rt.Status.ChartURL
16✔
323
                } else {
22✔
324
                        allRendered = false
6✔
325
                }
6✔
326
        }
327

328
        if pendingDeps {
16✔
329
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "MissingDependencies",
×
330
                        "One or more bound Releases or ComponentVersions not found"); condErr != nil {
×
331
                        return ctrl.Result{}, condErr
×
332
                }
×
333

334
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
×
335
        }
336

337
        if !allRendered {
22✔
338
                if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionFalse, "Pending",
6✔
339
                        "Waiting for release RenderTasks to complete"); condErr != nil {
6✔
340
                        return ctrl.Result{}, condErr
×
341
                }
×
342

343
                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
6✔
344
        }
345

346
        if condErr := r.setCondition(ctx, target, ConditionTypeReleasesRendered, metav1.ConditionTrue, "AllRendered",
10✔
347
                "All releases rendered successfully"); condErr != nil {
10✔
348
                return ctrl.Result{}, condErr
×
349
        }
×
350

351
        // Determine if a new bootstrap render is needed by checking whether the
352
        // current bootstrapVersion's RenderTask still matches the desired release set.
353
        bootstrapVersion := target.Status.BootstrapVersion
10✔
354
        bootstrapRTName := targetRenderTaskName(target.Name, bootstrapVersion)
10✔
355
        bootstrapRT := &solarv1alpha1.RenderTask{}
10✔
356
        err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT)
10✔
357

10✔
358
        needsNewBootstrap := false
10✔
359

10✔
360
        switch {
10✔
361
        case apierrors.IsNotFound(err):
1✔
362
                // No RenderTask for the current version yet — create one
1✔
363
                needsNewBootstrap = true
1✔
364
        case err != nil:
×
365
                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get bootstrap RenderTask")
×
366
        default:
9✔
367
                // RenderTask exists — check if the desired bootstrap input changed
9✔
368
                // (release set, resolved refs/tags, or userdata)
9✔
369
                desiredInput, inputErr := buildBootstrapInput(target, releases)
9✔
370
                if inputErr != nil {
9✔
371
                        return ctrl.Result{}, errLogAndWrap(log, inputErr, "failed to build desired bootstrap input for comparison")
×
372
                }
×
373

374
                existingInput := bootstrapRT.Spec.RendererConfig.BootstrapConfig.Input
9✔
375
                if !apiequality.Semantic.DeepEqual(desiredInput, existingInput) {
11✔
376
                        bootstrapVersion++
2✔
377
                        needsNewBootstrap = true
2✔
378
                }
2✔
379
        }
380

381
        if needsNewBootstrap {
13✔
382
                spec, specErr := r.computeBootstrapRenderTaskSpec(target, releases, registry, bootstrapVersion)
3✔
383
                if specErr != nil {
3✔
384
                        return ctrl.Result{}, errLogAndWrap(log, specErr, "failed to compute bootstrap RenderTask spec")
×
385
                }
×
386

387
                bootstrapRTName = targetRenderTaskName(target.Name, bootstrapVersion)
3✔
388
                bootstrapRT = &solarv1alpha1.RenderTask{
3✔
389
                        ObjectMeta: metav1.ObjectMeta{
3✔
390
                                Name:      bootstrapRTName,
3✔
391
                                Namespace: target.Namespace,
3✔
392
                        },
3✔
393
                        Spec: spec,
3✔
394
                }
3✔
395

3✔
396
                if err := r.Create(ctx, bootstrapRT); err != nil {
4✔
397
                        if !apierrors.IsAlreadyExists(err) {
1✔
398
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to create bootstrap RenderTask")
×
399
                        }
×
400

401
                        if err := r.Get(ctx, client.ObjectKey{Name: bootstrapRTName, Namespace: target.Namespace}, bootstrapRT); err != nil {
1✔
402
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to get existing bootstrap RenderTask")
×
403
                        }
×
404
                } else {
2✔
405
                        log.V(1).Info("Created bootstrap RenderTask", "renderTask", bootstrapRTName, "bootstrapVersion", bootstrapVersion)
2✔
406
                        r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Created", "Create",
2✔
407
                                "Created bootstrap RenderTask %s (version %d)", bootstrapRTName, bootstrapVersion)
2✔
408
                }
2✔
409

410
                // Persist the new bootstrapVersion in status
411
                if bootstrapVersion != target.Status.BootstrapVersion {
5✔
412
                        target.Status.BootstrapVersion = bootstrapVersion
2✔
413
                        if err := r.Status().Update(ctx, target); err != nil {
3✔
414
                                return ctrl.Result{}, errLogAndWrap(log, err, "failed to update Target bootstrapVersion")
1✔
415
                        }
1✔
416
                }
417
        }
418

419
        // Update target status from bootstrap RenderTask
420
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobFailed) {
9✔
421
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionFalse, "Failed",
×
422
                        "Bootstrap rendering failed"); condErr != nil {
×
423
                        return ctrl.Result{}, condErr
×
424
                }
×
425

426
                return ctrl.Result{}, nil
×
427
        }
428

429
        if apimeta.IsStatusConditionTrue(bootstrapRT.Status.Conditions, ConditionTypeJobSucceeded) {
13✔
430
                if condErr := r.setCondition(ctx, target, ConditionTypeBootstrapReady, metav1.ConditionTrue, "Ready",
4✔
431
                        "Bootstrap rendered successfully: "+bootstrapRT.Status.ChartURL); condErr != nil {
4✔
432
                        return ctrl.Result{}, condErr
×
433
                }
×
434

435
                // Clean up stale RenderTasks owned by this target (old versions)
436
                currentRTNames := map[string]struct{}{bootstrapRTName: {}}
4✔
437
                for _, ri := range releases {
10✔
438
                        currentRTNames[ri.rtName] = struct{}{}
6✔
439
                }
6✔
440
                if err := r.deleteStaleRenderTasks(ctx, target, currentRTNames); err != nil {
4✔
441
                        log.Error(err, "failed to clean up stale RenderTasks")
×
442
                }
×
443

444
                return ctrl.Result{}, nil
4✔
445
        }
446

447
        // Still running
448
        return ctrl.Result{}, nil
5✔
449
}
450

451
func (r *TargetReconciler) setCondition(ctx context.Context, target *solarv1alpha1.Target, condType string, status metav1.ConditionStatus, reason, message string) error {
67✔
452
        changed := apimeta.SetStatusCondition(&target.Status.Conditions, metav1.Condition{
67✔
453
                Type:               condType,
67✔
454
                Status:             status,
67✔
455
                ObservedGeneration: target.Generation,
67✔
456
                Reason:             reason,
67✔
457
                Message:            message,
67✔
458
        })
67✔
459
        if changed {
92✔
460
                if err := r.Status().Update(ctx, target); err != nil {
26✔
461
                        return fmt.Errorf("failed to update Target status condition %s: %w", condType, err)
1✔
462
                }
1✔
463
        }
464

465
        return nil
66✔
466
}
467

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

4✔
474
        rtList := &solarv1alpha1.RenderTaskList{}
4✔
475
        if err := r.List(ctx, rtList,
4✔
476
                client.InNamespace(target.Namespace),
4✔
477
                client.MatchingFields{indexOwnerKind: "Target"},
4✔
478
        ); err != nil {
4✔
479
                return err
×
480
        }
×
481

482
        for i := range rtList.Items {
15✔
483
                rt := &rtList.Items[i]
11✔
484
                if rt.Spec.OwnerName != target.Name || rt.Spec.OwnerNamespace != target.Namespace {
11✔
485
                        continue
×
486
                }
487

488
                if _, current := currentRTNames[rt.Name]; current {
21✔
489
                        continue
10✔
490
                }
491

492
                log.V(1).Info("Deleting stale RenderTask", "renderTask", rt.Name)
1✔
493
                if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
1✔
494
                        return err
×
495
                }
×
496

497
                r.Recorder.Eventf(target, nil, corev1.EventTypeNormal, "Deleted", "Delete",
1✔
498
                        "Deleted stale RenderTask %s", rt.Name)
1✔
499
        }
500

501
        return nil
4✔
502
}
503

504
func (r *TargetReconciler) deleteOwnedRenderTasks(ctx context.Context, target *solarv1alpha1.Target) error {
1✔
505
        rtList := &solarv1alpha1.RenderTaskList{}
1✔
506
        if err := r.List(ctx, rtList,
1✔
507
                client.InNamespace(target.Namespace),
1✔
508
                client.MatchingFields{indexOwnerKind: "Target"},
1✔
509
        ); err != nil {
1✔
510
                return err
×
511
        }
×
512

513
        for i := range rtList.Items {
1✔
514
                rt := &rtList.Items[i]
×
515
                if rt.Spec.OwnerName == target.Name && rt.Spec.OwnerNamespace == target.Namespace {
×
516
                        if err := r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
×
517
                                return err
×
518
                        }
×
519
                }
520
        }
521

522
        return nil
1✔
523
}
524

525
func (r *TargetReconciler) computeReleaseRenderTaskSpec(rel *solarv1alpha1.Release, cv *solarv1alpha1.ComponentVersion, registry *solarv1alpha1.Registry, target *solarv1alpha1.Target) solarv1alpha1.RenderTaskSpec {
3✔
526
        chartName := fmt.Sprintf("release-%s", rel.Name)
3✔
527
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
3✔
528
        tag := fmt.Sprintf("v0.0.%d", rel.GetGeneration())
3✔
529

3✔
530
        var targetNamespace string
3✔
531
        if rel.Spec.TargetNamespace != nil {
6✔
532
                targetNamespace = *rel.Spec.TargetNamespace
3✔
533
        }
3✔
534

535
        return solarv1alpha1.RenderTaskSpec{
3✔
536
                RendererConfig: solarv1alpha1.RendererConfig{
3✔
537
                        Type: solarv1alpha1.RendererConfigTypeRelease,
3✔
538
                        ReleaseConfig: solarv1alpha1.ReleaseConfig{
3✔
539
                                Chart: solarv1alpha1.ChartConfig{
3✔
540
                                        Name:        chartName,
3✔
541
                                        Description: fmt.Sprintf("Release of %s", rel.Spec.ComponentVersionRef.Name),
3✔
542
                                        Version:     tag,
3✔
543
                                        AppVersion:  tag,
3✔
544
                                },
3✔
545
                                Input: solarv1alpha1.ReleaseInput{
3✔
546
                                        Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
3✔
547
                                        Resources:  cv.Spec.Resources,
3✔
548
                                        Entrypoint: cv.Spec.Entrypoint,
3✔
549
                                },
3✔
550
                                Values:          rel.Spec.Values,
3✔
551
                                TargetNamespace: targetNamespace,
3✔
552
                        },
3✔
553
                },
3✔
554
                Repository:     repo,
3✔
555
                Tag:            tag,
3✔
556
                BaseURL:        registry.Spec.Hostname,
3✔
557
                PushSecretRef:  registry.Spec.SolarSecretRef,
3✔
558
                FailedJobTTL:   rel.Spec.FailedJobTTL,
3✔
559
                OwnerName:      target.Name,
3✔
560
                OwnerNamespace: target.Namespace,
3✔
561
                OwnerKind:      "Target",
3✔
562
        }
3✔
563
}
564

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

12✔
570
        for _, ri := range releases {
31✔
571
                ref, err := ociname.ParseReference(ri.chartURL)
19✔
572
                if err != nil {
19✔
573
                        return solarv1alpha1.BootstrapInput{}, fmt.Errorf("failed to parse chartURL %s: %w", ri.chartURL, err)
×
574
                }
×
575

576
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
19✔
577
                if err != nil {
19✔
578
                        return solarv1alpha1.BootstrapInput{}, err
×
579
                }
×
580

581
                resolvedReleases[ri.name] = solarv1alpha1.ResourceAccess{
19✔
582
                        Repository: strings.TrimPrefix(repo, "oci://"),
19✔
583
                        Tag:        ref.Identifier(),
19✔
584
                }
19✔
585
        }
586

587
        return solarv1alpha1.BootstrapInput{
12✔
588
                Releases: resolvedReleases,
12✔
589
                Userdata: target.Spec.Userdata,
12✔
590
        }, nil
12✔
591
}
592

593
func (r *TargetReconciler) computeBootstrapRenderTaskSpec(target *solarv1alpha1.Target, releases []releaseInfo, registry *solarv1alpha1.Registry, bootstrapVersion int64) (solarv1alpha1.RenderTaskSpec, error) {
3✔
594
        input, err := buildBootstrapInput(target, releases)
3✔
595
        if err != nil {
3✔
596
                return solarv1alpha1.RenderTaskSpec{}, err
×
597
        }
×
598

599
        releaseNames := make([]string, 0, len(releases))
3✔
600
        for _, ri := range releases {
8✔
601
                releaseNames = append(releaseNames, ri.name)
5✔
602
        }
5✔
603

604
        sort.Strings(releaseNames)
3✔
605

3✔
606
        chartName := fmt.Sprintf("bootstrap-%s", target.Name)
3✔
607
        repo := fmt.Sprintf("%s/%s", target.Namespace, chartName)
3✔
608
        tag := fmt.Sprintf("v0.0.%d", bootstrapVersion)
3✔
609

3✔
610
        return solarv1alpha1.RenderTaskSpec{
3✔
611
                RendererConfig: solarv1alpha1.RendererConfig{
3✔
612
                        Type: solarv1alpha1.RendererConfigTypeBootstrap,
3✔
613
                        BootstrapConfig: solarv1alpha1.BootstrapConfig{
3✔
614
                                Chart: solarv1alpha1.ChartConfig{
3✔
615
                                        Name:        chartName,
3✔
616
                                        Description: fmt.Sprintf("Bootstrap of %v", releaseNames),
3✔
617
                                        Version:     tag,
3✔
618
                                        AppVersion:  tag,
3✔
619
                                },
3✔
620
                                Input: input,
3✔
621
                        },
3✔
622
                },
3✔
623
                Repository:     repo,
3✔
624
                Tag:            tag,
3✔
625
                BaseURL:        registry.Spec.Hostname,
3✔
626
                PushSecretRef:  registry.Spec.SolarSecretRef,
3✔
627
                OwnerName:      target.Name,
3✔
628
                OwnerNamespace: target.Namespace,
3✔
629
                OwnerKind:      "Target",
3✔
630
        }, nil
3✔
631
}
632

633
// SetupWithManager sets up the controller with the Manager.
634
func (r *TargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
635
        return ctrl.NewControllerManagedBy(mgr).
1✔
636
                For(&solarv1alpha1.Target{}).
1✔
637
                Watches(
1✔
638
                        &solarv1alpha1.ReleaseBinding{},
1✔
639
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseBindingToTarget),
1✔
640
                ).
1✔
641
                Watches(
1✔
642
                        &solarv1alpha1.RenderTask{},
1✔
643
                        handler.EnqueueRequestsFromMapFunc(mapRenderTaskToOwner("Target")),
1✔
644
                        builder.WithPredicates(renderTaskStatusChangePredicate()),
1✔
645
                ).
1✔
646
                Watches(
1✔
647
                        &solarv1alpha1.Registry{},
1✔
648
                        handler.EnqueueRequestsFromMapFunc(r.mapRegistryToTargets),
1✔
649
                ).
1✔
650
                Watches(
1✔
651
                        &solarv1alpha1.ReferenceGrant{},
1✔
652
                        handler.EnqueueRequestsFromMapFunc(r.mapReferenceGrantToTargets),
1✔
653
                ).
1✔
654
                Watches(
1✔
655
                        &solarv1alpha1.Release{},
1✔
656
                        handler.EnqueueRequestsFromMapFunc(r.mapReleaseToTargets),
1✔
657
                ).
1✔
658
                Complete(r)
1✔
659
}
1✔
660

661
// registryGranted checks whether a ReferenceGrant in registryNamespace permits
662
// fromNamespace to reference the named registry.
663
func (r *TargetReconciler) registryGranted(ctx context.Context, registryNamespace, fromNamespace string) (bool, error) {
×
664
        grantList := &solarv1alpha1.ReferenceGrantList{}
×
665
        if err := r.List(ctx, grantList, client.InNamespace(registryNamespace)); err != nil {
×
666
                return false, err
×
667
        }
×
668
        for i := range grantList.Items {
×
669
                grant := &grantList.Items[i]
×
670
                if grantPermitsRegistryAccess(grant, fromNamespace) {
×
671
                        return true, nil
×
672
                }
×
673
        }
674

675
        return false, nil
×
676
}
677

678
// grantPermitsRegistryAccess returns true if the ReferenceGrant allows a Target in
679
// fromNamespace to reference Registry resources in the grant's namespace.
680
func grantPermitsRegistryAccess(grant *solarv1alpha1.ReferenceGrant, fromNamespace string) bool {
×
681
        return grantPermits(grant, solarGroup, "Target", fromNamespace, solarGroup, "Registry")
×
682
}
×
683

684
// mapRegistryToTargets maps a Registry event to reconcile requests for all
685
// Targets that reference it — either in the same namespace or cross-namespace.
686
func (r *TargetReconciler) mapRegistryToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
6✔
687
        reg, ok := obj.(*solarv1alpha1.Registry)
6✔
688
        if !ok {
6✔
689
                return nil
×
690
        }
×
691

692
        // Same-namespace targets
693
        targetList := &solarv1alpha1.TargetList{}
6✔
694
        if err := r.List(ctx, targetList, client.InNamespace(reg.Namespace)); err != nil {
6✔
695
                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for Registry", "registry", reg.Name)
×
696

×
697
                return nil
×
698
        }
×
699

700
        var requests []reconcile.Request
6✔
701
        for _, t := range targetList.Items {
6✔
702
                if t.Spec.RenderRegistryRef.Name == reg.Name &&
×
703
                        (t.Spec.RenderRegistryNamespace == "" || t.Spec.RenderRegistryNamespace == reg.Namespace) {
×
704
                        requests = append(requests, reconcile.Request{
×
705
                                NamespacedName: types.NamespacedName{
×
706
                                        Name:      t.Name,
×
707
                                        Namespace: t.Namespace,
×
708
                                },
×
709
                        })
×
710
                }
×
711
        }
712

713
        // Cross-namespace targets: find namespaces that have been granted access to
714
        // registries in reg.Namespace, then check their targets.
715
        grantList := &solarv1alpha1.ReferenceGrantList{}
6✔
716
        if err := r.List(ctx, grantList, client.InNamespace(reg.Namespace)); err != nil {
6✔
717
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReferenceGrants for cross-namespace Registry mapping")
×
718
                return requests
×
719
        }
×
720

721
        for i := range grantList.Items {
6✔
722
                grant := &grantList.Items[i]
×
723
                if !grantsRegistryResource(grant) {
×
724
                        continue
×
725
                }
726
                for _, from := range grant.Spec.From {
×
727
                        if from.Kind != "Target" || from.Group != solarGroup {
×
728
                                continue
×
729
                        }
730
                        crossTargets := &solarv1alpha1.TargetList{}
×
731
                        if err := r.List(ctx, crossTargets, client.InNamespace(from.Namespace)); err != nil {
×
732
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list cross-namespace Targets", "namespace", from.Namespace)
×
733
                                continue
×
734
                        }
735
                        for _, t := range crossTargets.Items {
×
736
                                if t.Spec.RenderRegistryRef.Name == reg.Name && t.Spec.RenderRegistryNamespace == reg.Namespace {
×
737
                                        requests = append(requests, reconcile.Request{
×
738
                                                NamespacedName: types.NamespacedName{
×
739
                                                        Name:      t.Name,
×
740
                                                        Namespace: t.Namespace,
×
741
                                                },
×
742
                                        })
×
743
                                }
×
744
                        }
745
                }
746
        }
747

748
        return requests
6✔
749
}
750

751
// mapReferenceGrantToTargets enqueues Targets affected by a ReferenceGrant change
752
// either because the grant controls Registry access (Target → Registry) or because
753
// it controls ComponentVersion access (Release → ComponentVersion, Releases live in
754
// the same namespace as their Targets).
755
func (r *TargetReconciler) mapReferenceGrantToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
8✔
756
        grant, ok := obj.(*solarv1alpha1.ReferenceGrant)
8✔
757
        if !ok {
8✔
758
                return nil
×
759
        }
×
760

761
        var requests []reconcile.Request
8✔
762

8✔
763
        if grantsRegistryResource(grant) {
8✔
764
                for _, from := range grant.Spec.From {
×
765
                        if from.Kind != "Target" || from.Group != solarGroup {
×
766
                                continue
×
767
                        }
768
                        targets := &solarv1alpha1.TargetList{}
×
769
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
×
770
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ReferenceGrant mapping", "namespace", from.Namespace)
×
771
                                continue
×
772
                        }
773
                        for _, t := range targets.Items {
×
774
                                // Enqueue targets that reference a registry specifically in the grant's namespace
×
775
                                if t.Spec.RenderRegistryNamespace == grant.Namespace {
×
776
                                        requests = append(requests, reconcile.Request{
×
777
                                                NamespacedName: types.NamespacedName{
×
778
                                                        Name:      t.Name,
×
779
                                                        Namespace: t.Namespace,
×
780
                                                },
×
781
                                        })
×
782
                                }
×
783
                        }
784
                }
785
        }
786

787
        if grantsComponentVersionResource(grant) {
12✔
788
                for _, from := range grant.Spec.From {
8✔
789
                        if from.Kind != "Release" || from.Group != solarGroup {
4✔
790
                                continue
×
791
                        }
792
                        // Releases and Targets are co-located: list Targets in the Release's namespace.
793
                        targets := &solarv1alpha1.TargetList{}
4✔
794
                        if err := r.List(ctx, targets, client.InNamespace(from.Namespace)); err != nil {
4✔
795
                                ctrl.LoggerFrom(ctx).Error(err, "failed to list Targets for ComponentVersion grant mapping", "namespace", from.Namespace)
×
796
                                continue
×
797
                        }
798
                        for _, t := range targets.Items {
4✔
799
                                requests = append(requests, reconcile.Request{
×
800
                                        NamespacedName: types.NamespacedName{
×
801
                                                Name:      t.Name,
×
802
                                                Namespace: t.Namespace,
×
803
                                        },
×
804
                                })
×
805
                        }
×
806
                }
807
        }
808

809
        return requests
8✔
810
}
811

812
// grantsRegistryResource returns true if the ReferenceGrant includes Registry in its To list.
813
func grantsRegistryResource(grant *solarv1alpha1.ReferenceGrant) bool {
8✔
814
        for _, t := range grant.Spec.To {
16✔
815
                if t.Kind == "Registry" && t.Group == solarGroup {
8✔
816
                        return true
×
817
                }
×
818
        }
819

820
        return false
8✔
821
}
822

823
// mapReleaseToTargets maps a Release event to reconcile requests for all
824
// Targets that are bound to the release via ReleaseBindings.
825
func (r *TargetReconciler) mapReleaseToTargets(ctx context.Context, obj client.Object) []reconcile.Request {
33✔
826
        rel, ok := obj.(*solarv1alpha1.Release)
33✔
827
        if !ok {
33✔
828
                return nil
×
829
        }
×
830

831
        bindingList := &solarv1alpha1.ReleaseBindingList{}
33✔
832
        if err := r.List(ctx, bindingList,
33✔
833
                client.InNamespace(rel.Namespace),
33✔
834
                client.MatchingFields{indexReleaseBindingReleaseName: rel.Name},
33✔
835
        ); err != nil {
33✔
836
                ctrl.LoggerFrom(ctx).Error(err, "failed to list ReleaseBindings for Release", "release", rel.Name)
×
837

×
838
                return nil
×
839
        }
×
840

841
        seen := map[string]struct{}{}
33✔
842
        var requests []reconcile.Request
33✔
843

33✔
844
        for _, rb := range bindingList.Items {
33✔
845
                targetName := rb.Spec.TargetRef.Name
×
846
                if _, ok := seen[targetName]; ok {
×
847
                        continue
×
848
                }
849

850
                seen[targetName] = struct{}{}
×
851
                requests = append(requests, reconcile.Request{
×
852
                        NamespacedName: types.NamespacedName{
×
853
                                Name:      targetName,
×
854
                                Namespace: rb.Namespace,
×
855
                        },
×
856
                })
×
857
        }
858

859
        return requests
33✔
860
}
861

862
func (r *TargetReconciler) mapReleaseBindingToTarget(_ context.Context, obj client.Object) []reconcile.Request {
14✔
863
        rb, ok := obj.(*solarv1alpha1.ReleaseBinding)
14✔
864
        if !ok || rb.Spec.TargetRef.Name == "" {
14✔
865
                return nil
×
866
        }
×
867

868
        return []reconcile.Request{
14✔
869
                {
14✔
870
                        NamespacedName: types.NamespacedName{
14✔
871
                                Name:      rb.Spec.TargetRef.Name,
14✔
872
                                Namespace: rb.Namespace,
14✔
873
                        },
14✔
874
                },
14✔
875
        }
14✔
876
}
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