• 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

86.85
/pkg/controller/hydratedtarget_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
        "strings"
12
        "time"
13

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

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

28
const (
29
        hydratedTargetFinalizer = "solar.opendefense.cloud/hydrated-target-finalizer"
30
)
31

32
// HydratedTargetReconciler reconciles a HydratedTarget object
33
type HydratedTargetReconciler struct {
34
        client.Client
35
        Scheme   *runtime.Scheme
36
        Recorder events.EventRecorder
37
}
38

39
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets,verbs=get;list;watch;create;update;patch;delete
40
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets/status,verbs=get;update;patch
41
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=hydratedtargets/finalizers,verbs=update
42
// FIXME: Switch out releases for profiles                      👇
43
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=releases,verbs=get;list;watch
44
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
45
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
46
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
47

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

62
func (r *HydratedTargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
167✔
63
        log := ctrl.LoggerFrom(ctx)
167✔
64
        ctrlResult := ctrl.Result{}
167✔
65

167✔
66
        log.V(1).Info("HydratedTarget is being reconciled", "req", req)
167✔
67

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

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

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

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

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

99
                return ctrlResult, nil
2✔
100
        }
101

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

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

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

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

137
        if apierrors.IsNotFound(err) {
174✔
138
                if err := r.createRenderTask(ctx, res); err != nil {
41✔
139
                        log.V(1).Info("Failed to create RenderTask", "res", res)
9✔
140
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "failed to create RenderTask")
9✔
141

9✔
142
                        if apierrors.IsNotFound(err) {
15✔
143
                                return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
6✔
144
                        }
6✔
145

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

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

158
        // RenderTask still running, requeue
159
        return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
133✔
160
}
161

162
func (r *HydratedTargetReconciler) updateStatusConditionsFromRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget, rt *solarv1alpha1.RenderTask) (changed bool) {
133✔
163
        if rt == nil || res == nil {
133✔
164
                return false
×
165
        }
×
166

167
        log := ctrl.LoggerFrom(ctx)
133✔
168

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

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

1✔
181
                return changed
1✔
182
        }
1✔
183

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

1✔
193
                log.V(1).Info("RenderTask completed", "name", rt.Name)
1✔
194
                r.Recorder.Eventf(res, rt, corev1.EventTypeWarning, "TaskCompleted", "RunTask", "RenderTask completed successfully")
1✔
195

1✔
196
                return changed
1✔
197
        }
1✔
198

199
        log.V(1).Info("RenderTask has no final condtions yet", "name", rt.Name)
131✔
200

131✔
201
        return false
131✔
202
}
203

204
func (r *HydratedTargetReconciler) createRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget) error {
32✔
205
        log := ctrl.LoggerFrom(ctx)
32✔
206

32✔
207
        // Check if we need to cleanup an old task
32✔
208
        if res.Status.RenderTaskRef != nil && res.Status.RenderTaskRef.Name != "" {
40✔
209
                if err := r.deleteRenderTask(ctx, res); err != nil {
8✔
210
                        return errLogAndWrap(log, err, "failed to cleanup old task")
×
211
                }
×
212
        }
213

214
        spec, err := r.computeRenderTaskSpec(ctx, res)
32✔
215
        if err != nil {
38✔
216
                return err
6✔
217
        }
6✔
218
        rt := &solarv1alpha1.RenderTask{
26✔
219
                ObjectMeta: metav1.ObjectMeta{
26✔
220
                        Name:      generationName(res),
26✔
221
                        Namespace: res.Namespace,
26✔
222
                },
26✔
223
                Spec: spec,
26✔
224
        }
26✔
225

26✔
226
        if err := r.Create(ctx, rt); err != nil {
26✔
227
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create RenderTask", err)
×
228
                return errLogAndWrap(log, err, "secret creation failed")
×
229
        }
×
230

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

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

26✔
244
        if err := r.Status().Update(ctx, res); err != nil {
29✔
245
                return errLogAndWrap(log, err, "failed to update status")
3✔
246
        }
3✔
247

248
        return nil
23✔
249
}
250

251
func (r *HydratedTargetReconciler) deleteRenderTask(ctx context.Context, res *solarv1alpha1.HydratedTarget) error {
10✔
252
        if res.Status.RenderTaskRef == nil {
11✔
253
                return nil
1✔
254
        }
1✔
255

256
        rt := &solarv1alpha1.RenderTask{}
9✔
257
        if err := r.Get(ctx, client.ObjectKey{Name: res.Status.RenderTaskRef.Name, Namespace: res.Status.RenderTaskRef.Namespace}, rt); client.IgnoreNotFound(err) != nil {
9✔
258
                return err
×
259
        } else if err == nil {
17✔
260
                return r.Delete(ctx, rt, client.PropagationPolicy(metav1.DeletePropagationBackground))
8✔
261
        }
8✔
262

263
        return nil
1✔
264
}
265

266
func (r *HydratedTargetReconciler) computeRenderTaskSpec(ctx context.Context, res *solarv1alpha1.HydratedTarget) (solarv1alpha1.RenderTaskSpec, error) {
32✔
267
        spec := solarv1alpha1.RenderTaskSpec{}
32✔
268

32✔
269
        resolvedReleases := map[string]solarv1alpha1.ResourceAccess{}
32✔
270
        for k, v := range res.Spec.Releases {
47✔
271
                rel := &solarv1alpha1.Release{}
15✔
272
                if err := r.Get(ctx, client.ObjectKey{Name: v.Name, Namespace: res.Namespace}, rel); err != nil {
21✔
273
                        return spec, err
6✔
274
                }
6✔
275

276
                ref, err := ociname.ParseReference(rel.Status.ChartURL)
9✔
277
                if err != nil {
9✔
278
                        return spec, err
×
279
                }
×
280

281
                repo, err := url.JoinPath(ref.Context().RegistryStr(), ref.Context().RepositoryStr())
9✔
282
                if err != nil {
9✔
283
                        return spec, err
×
284
                }
×
285

286
                resolvedReleases[k] = solarv1alpha1.ResourceAccess{
9✔
287
                        Repository: strings.TrimPrefix(repo, "oci://"),
9✔
288
                        Tag:        ref.Identifier(),
9✔
289
                }
9✔
290
        }
291

292
        resolvedReleaseNames := []string{}
26✔
293
        for k := range resolvedReleases {
35✔
294
                resolvedReleaseNames = append(resolvedReleaseNames, k)
9✔
295
        }
9✔
296

297
        chartName := fmt.Sprintf("ht-%s", res.Name)
26✔
298
        repo, err := url.JoinPath(res.Namespace, chartName)
26✔
299
        if err != nil {
26✔
300
                return spec, err
×
301
        }
×
302

303
        tag := fmt.Sprintf("v0.0.%d", res.GetGeneration())
26✔
304

26✔
305
        spec.RendererConfig = solarv1alpha1.RendererConfig{
26✔
306
                Type: solarv1alpha1.RendererConfigTypeHydratedTarget,
26✔
307
                HydratedTargetConfig: solarv1alpha1.HydratedTargetConfig{
26✔
308
                        Chart: solarv1alpha1.ChartConfig{
26✔
309
                                Name:        chartName,
26✔
310
                                Description: fmt.Sprintf("HydratedTarget of %v", resolvedReleaseNames),
26✔
311
                                Version:     tag,
26✔
312
                                AppVersion:  tag,
26✔
313
                        },
26✔
314
                        Input: solarv1alpha1.HydratedTargetInput{
26✔
315
                                Releases: resolvedReleases,
26✔
316
                                Userdata: res.Spec.Userdata,
26✔
317
                        },
26✔
318
                },
26✔
319
        }
26✔
320
        spec.Repository = repo
26✔
321
        spec.Tag = tag
26✔
322

26✔
323
        return spec, nil
26✔
324
}
325

326
// SetupWithManager sets up the controller with the Manager.
327
func (r *HydratedTargetReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
328
        return ctrl.NewControllerManagedBy(mgr).
1✔
329
                For(&solarv1alpha1.HydratedTarget{}).
1✔
330
                Owns(&solarv1alpha1.RenderTask{}).
1✔
331
                Complete(r)
1✔
332
}
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