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

opendefensecloud / solution-arsenal / 22943463779

11 Mar 2026 08:25AM UTC coverage: 71.154% (+0.1%) from 71.014%
22943463779

push

github

web-flow
feat[#103]: Implement dev cluster (#180)

* Fix controller rbac
* feat: Implement kind dev cluster for manual testing
* fix: Explicitly set localhost as image location
* feat: Add permissions for events.k8s.io Events
We support both, core and events.k8s.io Events for compatibility. Our
kind dev-cluster is already using the new API.
* Cleartext credentials of htpasswd
* feat(dev-cluster): enhance dev cluster setup with zot discovery and ocm transfer support
* feat: Add cloud-provider-kind to devenv
* chore: We don't need minio
* fix: Hacky workaround for not ready webhook
* fix: Create namespace before using it
* fix: Wait with port-forward until zot is ready
* chore: Remove cloud-provider-kind again
Using ingress here would require messing with /etc/hosts or dnsmasq.
Let's stick with port-forward for now.
* feat: Push ocm package via https
* Add dev-cluster CA to gitignore
* quickfix: Webhook should listen on 0.0.0.0
* feat: Add webhook extension to zot
* test: Add discovery manifest for e2e testing
* ducttape: No leading slash in webhook.path
I think this should befixed in the discovery worker - not ducttape fixed
here. But for now, that layer of ducttape is enough to get it running.
* Start dev-cluster with local images
* Cleanup port-forward using ps
* refactor: Events extension only for zot-discovery
* refactor: Move static values to solar.values.yaml
* test: Add componentversion manifest for helmdemo 0.12.0
* Fix registryURL in discovery test resource
* feat: Add cluster-dns name of zot-deploy to zot-cert
* test: Add manifests used in manual e2e testing
Probably subject to change, so feel free to edit.
* feat: Add pushSecret for RenderTasks
* fix: Add push-secret for zot-deploy
* chore: Add caConfigMap to fixture solar.values.yaml
* refactor: Move body of make target "dev-cluster" to bash script
* chore: Disable shellcheck SC2086 twice
* testing: Make setting up discovery an explicit call
* tests: Add script to create release fixtures
* docs: Add... (continued)

2035 of 2860 relevant lines covered (71.15%)

21.24 hits per line

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

85.37
/pkg/controller/release_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
        "fmt"
9
        "net/url"
10
        "slices"
11
        "time"
12

13
        corev1 "k8s.io/api/core/v1"
14
        apierrors "k8s.io/apimachinery/pkg/api/errors"
15
        apimeta "k8s.io/apimachinery/pkg/api/meta"
16
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17
        "k8s.io/apimachinery/pkg/runtime"
18
        "k8s.io/apimachinery/pkg/types"
19
        "k8s.io/client-go/tools/events"
20
        ctrl "sigs.k8s.io/controller-runtime"
21
        "sigs.k8s.io/controller-runtime/pkg/client"
22
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
23

24
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
25
)
26

27
const (
28
        releaseFinalizer = "solar.opendefense.cloud/release-finalizer"
29
)
30

31
// ReleaseReconciler reconciles a Release object
32
type ReleaseReconciler struct {
33
        client.Client
34
        Scheme   *runtime.Scheme
35
        Recorder events.EventRecorder
36
}
37

38
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch;create;update;patch;delete
39
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/status,verbs=get;update;patch
40
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases/finalizers,verbs=update
41
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch
42
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
43
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
44
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
45

46
// Reconcile moves the current state of the cluster closer to the desired state
47
//
48
// Reconciliation Flow:
49
//
50
//        Release created
51
//            ↓
52
//        Add finalizer
53
//            ↓
54
//        Check if already succeeded → YES → Return (no-op)
55
//            ↓ NO
56
//        Get or create RenderTask
57
//            ↓
58
//        Update status from RenderTask
59

60
func (r *ReleaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
71✔
61
        log := ctrl.LoggerFrom(ctx)
71✔
62
        ctrlResult := ctrl.Result{}
71✔
63

71✔
64
        log.V(1).Info("Release is being reconciled", "req", req)
71✔
65

71✔
66
        // Fetch the Release instance
71✔
67
        res := &solarv1alpha1.Release{}
71✔
68
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
72✔
69
                if apierrors.IsNotFound(err) {
2✔
70
                        // Object not found, return. Created objects are automatically garbage collected.
1✔
71
                        return ctrlResult, nil
1✔
72
                }
1✔
73

74
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
75
        }
76

77
        // Handle deletion: cleanup rendertask, then remove finalizer
78
        if !res.DeletionTimestamp.IsZero() {
71✔
79
                log.V(1).Info("Release is being deleted")
1✔
80
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Release is being deleted, cleaning up resources")
1✔
81

1✔
82
                if err := r.deleteRenderTask(ctx, res); client.IgnoreNotFound(err) != nil {
1✔
83
                        return ctrlResult, errLogAndWrap(log, err, "failed to delete render task")
×
84
                }
×
85

86
                // Remove finalizer
87
                if slices.Contains(res.Finalizers, releaseFinalizer) {
2✔
88
                        log.V(1).Info("Removing finalizer from resource")
1✔
89
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
2✔
90
                                return f == releaseFinalizer
1✔
91
                        })
1✔
92
                        if err := r.Update(ctx, res); err != nil {
1✔
93
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
×
94
                        }
×
95
                }
96

97
                return ctrlResult, nil
1✔
98
        }
99

100
        // Add finalizer if not present and not deleting
101
        if res.DeletionTimestamp.IsZero() {
138✔
102
                if !slices.Contains(res.Finalizers, releaseFinalizer) {
83✔
103
                        log.V(1).Info("Adding finalizer to resource")
14✔
104
                        res.Finalizers = append(res.Finalizers, releaseFinalizer)
14✔
105
                        if err := r.Update(ctx, res); err != nil {
14✔
106
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
107
                        }
×
108
                        // Return without requeue; the Update event will trigger reconciliation again
109
                        return ctrlResult, nil
14✔
110
                }
111
        }
112

113
        // Check if rendertask has already completed successfully
114
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskCompleted)
55✔
115
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
56✔
116
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
1✔
117
                return ctrlResult, nil
1✔
118
        }
1✔
119

120
        // Check if rendertask has already failed
121
        fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeTaskFailed)
54✔
122
        if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue {
55✔
123
                log.V(1).Info("RenderTask has already failed, no further action needed")
1✔
124
                return ctrlResult, nil
1✔
125
        }
1✔
126

127
        // Reconcile RenderTask
128
        rt := &solarv1alpha1.RenderTask{}
53✔
129
        err := r.Get(ctx, client.ObjectKey{Name: generationName(res), Namespace: res.Namespace}, rt)
53✔
130
        if client.IgnoreNotFound(err) != nil {
53✔
131
                log.V(1).Info("Failed to get render task", "res", res)
×
132
                return ctrlResult, errLogAndWrap(log, err, "failed to get RenderTask")
×
133
        }
×
134

135
        if apierrors.IsNotFound(err) {
69✔
136
                if err := r.createRenderTask(ctx, res); err != nil {
24✔
137
                        log.V(1).Info("Failed to create RenderTask", "res", res)
8✔
138
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "failed to create RenderTask")
8✔
139

8✔
140
                        if apierrors.IsNotFound(err) {
15✔
141
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
7✔
142
                        }
7✔
143

144
                        return ctrlResult, errLogAndWrap(log, err, "failed to create RenderTask")
1✔
145
                }
146
                log.V(1).Info("Created RenderTask", "res", res)
8✔
147
                r.Recorder.Eventf(res, rt, corev1.EventTypeNormal, "Created", "Create", "RenderTask was created")
8✔
148
        }
149

150
        if changed := r.updateStatusConditionsFromRenderTask(ctx, res, rt); changed {
47✔
151
                if err := r.Status().Update(ctx, res); err != nil {
2✔
152
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
153
                }
×
154
        }
155

156
        // RenderTask still running, requeue
157
        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
45✔
158
}
159

160
func (r *ReleaseReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.Release, rt *solarv1alpha1.RenderTask) (changed bool) {
45✔
161
        if rt == nil || res == nil {
45✔
162
                return false
×
163
        }
×
164

165
        log := ctrl.LoggerFrom(ctx)
45✔
166

45✔
167
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobFailed) {
46✔
168
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
169
                        Type:               ConditionTypeTaskFailed,
1✔
170
                        Status:             metav1.ConditionTrue,
1✔
171
                        ObservedGeneration: res.Generation,
1✔
172
                        Reason:             "TaskFailed",
1✔
173
                        Message:            "RenderTask failed",
1✔
174
                })
1✔
175

1✔
176
                log.V(1).Info("RenderTask failed", "name", rt.Name)
1✔
177
                r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskFailed", "RunTask", "RenderTask failed")
1✔
178

1✔
179
                return changed
1✔
180
        }
1✔
181

182
        if apimeta.IsStatusConditionTrue(rt.Status.Conditions, ConditionTypeJobSucceeded) {
45✔
183
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
184
                        Type:               ConditionTypeTaskCompleted,
1✔
185
                        Status:             metav1.ConditionTrue,
1✔
186
                        ObservedGeneration: res.Generation,
1✔
187
                        Reason:             "TaskCompleted",
1✔
188
                        Message:            "RenderTask completed",
1✔
189
                })
1✔
190

1✔
191
                if res.Status.ChartURL != rt.Status.ChartURL {
1✔
192
                        res.Status.ChartURL = rt.Status.ChartURL
×
193
                        changed = true
×
194
                }
×
195

196
                log.V(1).Info("RenderTask succeeded", "name", rt.Name)
1✔
197
                r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskCompleted", "RunTask", "RenderTask completed successfully")
1✔
198

1✔
199
                return changed
1✔
200
        }
201

202
        log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name)
43✔
203

43✔
204
        return false
43✔
205
}
206

207
func (r *ReleaseReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.Release) error {
16✔
208
        log := ctrl.LoggerFrom(ctx)
16✔
209

16✔
210
        // Check if we need to cleanup an old task
16✔
211
        if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" {
17✔
212
                if err := r.deleteRenderTask(ctx, res); err != nil {
1✔
213
                        return errLogAndWrap(log, err, "failed to cleanup old task")
×
214
                }
×
215
        }
216

217
        spec, err := r.computeRenderTaskSpec(ctx, res)
16✔
218
        if err != nil {
23✔
219
                return err
7✔
220
        }
7✔
221
        rt := &solarv1alpha1.RenderTask{
9✔
222
                ObjectMeta: metav1.ObjectMeta{
9✔
223
                        Name:      generationName(res),
9✔
224
                        Namespace: res.Namespace,
9✔
225
                },
9✔
226
                Spec: spec,
9✔
227
        }
9✔
228

9✔
229
        if err := r.Create(ctx, rt); err != nil {
9✔
230
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err)
×
231
                return errLogAndWrap(log, err, "failed to create RenderTask")
×
232
        }
×
233

234
        // Set owner references
235
        if err := controllerutil.SetControllerReference(res, rt, r.Scheme); err != nil {
9✔
236
                return errLogAndWrap(log, err, "failed to set controller reference")
×
237
        }
×
238

239
        // Set Reference in Status
240
        res.Status.RenderTaskRef = &corev1.ObjectReference{
9✔
241
                APIVersion: solarv1alpha1.SchemeGroupVersion.String(),
9✔
242
                Kind:       "RenderTask",
9✔
243
                Namespace:  rt.Namespace,
9✔
244
                Name:       rt.Name,
9✔
245
        }
9✔
246

9✔
247
        if err := r.Status().Update(ctx, res); err != nil {
10✔
248
                return errLogAndWrap(log, err, "failed to update status")
1✔
249
        }
1✔
250

251
        return nil
8✔
252
}
253

254
func (r *ReleaseReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.Release) error {
2✔
255
        if res.Status.RenderTaskRef == nil {
2✔
256
                return nil
×
257
        }
×
258

259
        rt := &solarv1alpha1.RenderTask{}
2✔
260
        if err := r.Get(ctx, client.ObjectKey{Name: res.Status.RenderTaskRef.Name, Namespace: res.Status.RenderTaskRef.Namespace}, rt); client.IgnoreNotFound(err) != nil {
2✔
261
                return err
×
262
        } else if err == nil {
4✔
263
                return r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground))
2✔
264
        }
2✔
265

266
        return nil
×
267
}
268

269
func (r *ReleaseReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.Release) (solarv1alpha1.RenderTaskSpec, error) {
16✔
270
        spec := solarv1alpha1.RenderTaskSpec{}
16✔
271

16✔
272
        cvRef := types.NamespacedName{
16✔
273
                Name:      res.Spec.ComponentVersionRef.Name,
16✔
274
                Namespace: res.Namespace,
16✔
275
        }
16✔
276

16✔
277
        cv := &solarv1alpha1.ComponentVersion{}
16✔
278
        if err := r.Get(ctx, cvRef, cv); err != nil {
23✔
279
                return spec, err
7✔
280
        }
7✔
281

282
        chartName := fmt.Sprintf("release-%s", res.Name)
9✔
283
        repo, err := url.JoinPath(res.Namespace, chartName)
9✔
284
        if err != nil {
9✔
285
                return spec, err
×
286
        }
×
287

288
        tag := fmt.Sprintf("v0.0.%d", res.GetGeneration())
9✔
289

9✔
290
        spec.RendererConfig = solarv1alpha1.RendererConfig{
9✔
291
                Type: solarv1alpha1.RendererConfigTypeRelease,
9✔
292
                ReleaseConfig: solarv1alpha1.ReleaseConfig{
9✔
293
                        Chart: solarv1alpha1.ChartConfig{
9✔
294
                                Name:        chartName,
9✔
295
                                Description: fmt.Sprintf("Release of %s", res.Spec.ComponentVersionRef.Name),
9✔
296
                                Version:     tag,
9✔
297
                                AppVersion:  tag,
9✔
298
                        },
9✔
299
                        Input: solarv1alpha1.ReleaseInput{
9✔
300
                                Component:  solarv1alpha1.ReleaseComponent{Name: cv.Spec.ComponentRef.Name},
9✔
301
                                Resources:  cv.Spec.Resources,
9✔
302
                                Entrypoint: cv.Spec.Entrypoint,
9✔
303
                        },
9✔
304
                        Values: res.Spec.Values,
9✔
305
                },
9✔
306
        }
9✔
307
        spec.Repository = repo
9✔
308
        spec.Tag = tag
9✔
309

9✔
310
        return spec, nil
9✔
311
}
312

313
// SetupWithManager sets up the controller with the Manager.
314
func (r *ReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
315
        return ctrl.NewControllerManagedBy(mgr).
1✔
316
                For(&solarv1alpha1.Release{}).
1✔
317
                Owns(&solarv1alpha1.RenderTask{}).
1✔
318
                Complete(r)
1✔
319
}
1✔
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