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

opendefensecloud / solution-arsenal / 22098237703

17 Feb 2026 12:20PM UTC coverage: 71.236% (+0.5%) from 70.744%
22098237703

Pull #157

github

web-flow
Merge 73e63ad54 into bbcb7ff8f
Pull Request #157: Implement APIWriter

91 of 106 new or added lines in 3 files covered. (85.85%)

4 existing lines in 1 file now uncovered.

1694 of 2378 relevant lines covered (71.24%)

14.69 hits per line

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

67.23
/pkg/controller/discovery_controller.go
1
// Copyright 2026 BWI GmbH and Artefact Conduit contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "fmt"
9
        "slices"
10

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

22
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
23
        "go.opendefense.cloud/solar/pkg/discovery"
24
)
25

26
const (
27
        discoveryFinalizer = "solar.opendefense.cloud/discovery-finalizer"
28
)
29

30
// DiscoveryReconciler reconciles a Discovery object
31
type DiscoveryReconciler struct {
32
        client.Client
33
        Scheme        *runtime.Scheme
34
        Recorder      events.EventRecorder
35
        WorkerImage   string
36
        WorkerCommand string
37
        WorkerArgs    []string
38
}
39

40
// nolint:lll
41
// +kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries,verbs=get;list;watch;create;update;patch;delete
42
// +kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries/status,verbs=get;update;patch
43
// +kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries/finalizers,verbs=update
44
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
45
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
46
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
47

48
// Reconcile moves the current state of the cluster closer to the desired state
49
func (r *DiscoveryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
5✔
50
        log := ctrl.LoggerFrom(ctx)
5✔
51
        ctrlResult := ctrl.Result{}
5✔
52

5✔
53
        log.V(1).Info("Discovery is being reconciled", "req", req)
5✔
54

5✔
55
        // Fetch the Order instance
5✔
56
        res := &solarv1alpha1.Discovery{}
5✔
57
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
5✔
58
                if apierrors.IsNotFound(err) {
×
59
                        // Object not found, return. Created objects are automatically garbage collected.
×
60
                        return ctrlResult, nil
×
61
                }
×
62

63
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
64
        }
65

66
        // Handle deletion: cleanup artifact workflows, then remove finalizer
67
        if !res.DeletionTimestamp.IsZero() {
5✔
68
                log.V(1).Info("Discovery is being deleted")
×
69
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Discovery is being deleted, cleaning up worker")
×
70

×
71
                // Cleanup worker resources, if exists
×
72
                if err := r.deleteWorkerResources(ctx, res); err != nil {
×
73
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up worker resources")
×
74
                }
×
75

76
                // Remove finalizer
77
                if slices.Contains(res.Finalizers, discoveryFinalizer) {
×
78
                        log.V(1).Info("Removing finalizer from resource")
×
79
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
×
80
                                return f == discoveryFinalizer
×
81
                        })
×
82
                        if err := r.Update(ctx, res); err != nil {
×
83
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
×
84
                        }
×
85
                }
86

87
                return ctrlResult, nil
×
88
        }
89

90
        // Add finalizer if not present and not deleting
91
        if res.DeletionTimestamp.IsZero() {
10✔
92
                if !slices.Contains(res.Finalizers, discoveryFinalizer) {
6✔
93
                        log.V(1).Info("Adding finalizer to resource")
1✔
94
                        res.Finalizers = append(res.Finalizers, discoveryFinalizer)
1✔
95
                        if err := r.Update(ctx, res); err != nil {
1✔
96
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
97
                        }
×
98
                        // Return without requeue; the Update event will trigger reconciliation again
99
                        return ctrlResult, nil
1✔
100
                }
101
        }
102

103
        pod := &corev1.Pod{}
4✔
104
        err := r.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}, pod)
4✔
105
        if err != nil && !apierrors.IsNotFound(err) {
4✔
106
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodNotFound", "GetPod", "Failed to get pod", err)
×
107
                return ctrlResult, errLogAndWrap(log, err, "failed to get pod information")
×
108
        }
×
109

110
        // No pod yet, create it.
111
        if apierrors.IsNotFound(err) {
5✔
112
                if err := r.createWorkerResources(ctx, res); err != nil {
1✔
113
                        return ctrlResult, errLogAndWrap(log, err, "failed to create pod")
×
114
                }
×
115

116
                return ctrlResult, nil
1✔
117
        }
118

119
        // Pod exists, check if it's up to date with our configuration and if it is healthy.
120
        if res.Status.PodGeneration != res.GetGeneration() {
4✔
121
                // Recreate pod, configuration mismatch
1✔
122
                r.Recorder.Eventf(res, nil, corev1.EventTypeNormal, "ConfigurationChanged", "CompareConfiguration", "Configuration changed. Replacing pod.")
1✔
123
                if err := r.deleteWorkerResources(ctx, res); err != nil {
1✔
124
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up worker resources")
×
125
                }
×
126

127
                if err := r.createWorkerResources(ctx, res); err != nil {
1✔
UNCOV
128
                        return ctrlResult, errLogAndWrap(log, err, "failed to create pod")
×
UNCOV
129
                }
×
130

131
                return ctrlResult, nil
1✔
132
        } else {
2✔
133
                log.V(1).Info("Configuration hasn't changed", "podGen", res.Status.PodGeneration, "gen", res.GetGeneration())
2✔
134
        }
2✔
135

136
        return ctrlResult, nil
2✔
137
}
138

139
// deleteWorkerResources deletes the resources of the worker pod
140
func (r *DiscoveryReconciler) deleteWorkerResources(ctx context.Context, res *solarv1alpha1.Discovery) error {
1✔
141
        log := ctrl.LoggerFrom(ctx)
1✔
142

1✔
143
        if err := r.Delete(ctx, &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
1✔
144
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceDeletionFailed", "DeleteService", "Failed to delete service", err)
×
145
                return errLogAndWrap(log, err, "service deletion failed")
×
146
        }
×
147

148
        if err := r.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
1✔
149
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretDeletionFailed", "DeleteSecret", "Failed to delete secret", err)
×
150
                return errLogAndWrap(log, err, "secret deletion failed")
×
151
        }
×
152

153
        if err := r.Delete(ctx, &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
1✔
154
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodDeletionFailed", "DeletePod", "Failed to delete pod", err)
×
155
                return errLogAndWrap(log, err, "pod deletion failed")
×
156
        }
×
157

158
        return nil
1✔
159
}
160

161
// createWorkerResources creates the necessary resources for the worker pod
162
func (r *DiscoveryReconciler) createWorkerResources(ctx context.Context, res *solarv1alpha1.Discovery) error {
2✔
163
        log := ctrl.LoggerFrom(ctx)
2✔
164

2✔
165
        // Create secret
2✔
166
        secret := &corev1.Secret{
2✔
167
                ObjectMeta: objectMeta(res),
2✔
168
        }
2✔
169

2✔
170
        rp := discovery.NewRegistryProvider()
2✔
171
        reg := &discovery.Registry{
2✔
172
                Name:      res.Name,
2✔
173
                PlainHTTP: res.Spec.Registry.PlainHTTP,
2✔
174
                Hostname:  res.Spec.Registry.RegistryURL,
2✔
175
        }
2✔
176
        if res.Spec.Webhook != nil {
2✔
177
                reg.WebhookPath = res.Spec.Webhook.Path
×
178
                reg.Flavor = res.Spec.Webhook.Flavor
×
179
        }
×
180
        if res.Spec.DiscoveryInterval != nil {
3✔
181
                reg.ScanInterval = res.Spec.DiscoveryInterval.Duration
1✔
182
        }
1✔
183
        if err := rp.Register(reg); err != nil {
2✔
184
                return errLogAndWrap(log, err, "failed to register registry")
×
185
        }
×
186

187
        // Add credentials if specified
188
        if res.Spec.Registry.SecretRef.Name != "" {
2✔
189
                sec := &corev1.Secret{}
×
190
                if err := r.Get(ctx, types.NamespacedName{Name: res.Spec.Registry.SecretRef.Name, Namespace: res.Namespace}, sec); err != nil {
×
191
                        return errLogAndWrap(log, err, "failed to get registry secret")
×
192
                } else {
×
193
                        username, okUser := sec.Data["username"]
×
194
                        password, okPass := sec.Data["password"]
×
195
                        if okUser && okPass {
×
196
                                reg.Credentials = &discovery.RegistryCredentials{
×
197
                                        Username: string(username),
×
198
                                        Password: string(password),
×
199
                                }
×
200
                        } else {
×
201
                                return fmt.Errorf("registry secret is missing username or password fields")
×
202
                        }
×
203
                }
204
        }
205

206
        confData, err := rp.Marshal()
2✔
207
        if err != nil {
2✔
208
                return errLogAndWrap(log, err, "failed to marshal registry configuration")
×
209
        }
×
210
        secret.StringData = map[string]string{
2✔
211
                "config.yaml": string(confData),
2✔
212
        }
2✔
213

2✔
214
        // Create secret in cluster
2✔
215
        if err := r.Create(ctx, secret); err != nil {
2✔
216
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretCreationFailed", "CreateSecret", "Failed to create secret", err)
×
217
                return errLogAndWrap(log, err, "failed to create secret")
×
218
        }
×
219
        r.Recorder.Eventf(res, secret, corev1.EventTypeNormal, "SecretCreated", "CreateSecret", "Secret created")
2✔
220

2✔
221
        // Set owner references
2✔
222
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
2✔
223
                return errLogAndWrap(log, err, "failed to set controller reference")
×
224
        }
×
225

226
        // Create pod
227
        var args = r.WorkerArgs
2✔
228
        args = append(args, "--config", "/etc/worker/config.yaml")
2✔
229
        pod := &corev1.Pod{
2✔
230
                ObjectMeta: objectMeta(res),
2✔
231
                Spec: corev1.PodSpec{
2✔
232
                        Containers: []corev1.Container{
2✔
233
                                {
2✔
234
                                        Name:    "worker",
2✔
235
                                        Image:   r.WorkerImage,
2✔
236
                                        Command: []string{r.WorkerCommand},
2✔
237
                                        Args:    args,
2✔
238
                                        VolumeMounts: []corev1.VolumeMount{
2✔
239
                                                {
2✔
240
                                                        Name:      "config",
2✔
241
                                                        ReadOnly:  true,
2✔
242
                                                        MountPath: "/etc/worker"},
2✔
243
                                        },
2✔
244
                                        Ports: []corev1.ContainerPort{
2✔
245
                                                {
2✔
246
                                                        Name:          "webhook",
2✔
247
                                                        ContainerPort: 8080,
2✔
248
                                                },
2✔
249
                                        },
2✔
250
                                },
2✔
251
                        },
2✔
252
                        Volumes: []corev1.Volume{
2✔
253
                                {
2✔
254
                                        Name: "config",
2✔
255
                                        VolumeSource: corev1.VolumeSource{
2✔
256
                                                Secret: &corev1.SecretVolumeSource{
2✔
257
                                                        SecretName: res.Name,
2✔
258
                                                },
2✔
259
                                        },
2✔
260
                                },
2✔
261
                        },
2✔
262
                },
2✔
263
        }
2✔
264

2✔
265
        // Set owner references
2✔
266
        if err := controllerutil.SetControllerReference(res, pod, r.Scheme); err != nil {
2✔
267
                return errLogAndWrap(log, err, "failed to set controller reference")
×
268
        }
×
269

270
        // Create pod in cluster
271
        if err := r.Create(ctx, pod); err != nil {
2✔
272
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodCreationFailed", "CreatePod", "Failed to create pod", err)
×
273
                return errLogAndWrap(log, err, "failed to create pod")
×
274
        }
×
275
        r.Recorder.Eventf(res, pod, corev1.EventTypeNormal, "PodCreated", "CreatePod", "Pod created")
2✔
276
        log.V(1).Info("Pod created", "podGen", res.GetGeneration())
2✔
277

2✔
278
        // Create service
2✔
279
        svc := &corev1.Service{
2✔
280
                ObjectMeta: objectMeta(res),
2✔
281
                Spec: corev1.ServiceSpec{
2✔
282
                        Type:     corev1.ServiceTypeClusterIP,
2✔
283
                        Ports:    []corev1.ServicePort{{Name: "webhook", Port: 8080, TargetPort: intstr.FromString("webhook")}},
2✔
284
                        Selector: map[string]string{"app.kubernetes.io/name": discoveryPrefixed(res.Name)},
2✔
285
                },
2✔
286
        }
2✔
287

2✔
288
        if err := r.Create(ctx, svc); err != nil {
2✔
289
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceCreationFailed", "CreateService", "Failed to create service", err)
×
290
                return errLogAndWrap(log, err, "failed to create service")
×
291
        }
×
292
        r.Recorder.Eventf(res, svc, corev1.EventTypeNormal, "ServiceCreated", "CreateService", "Service created")
2✔
293

2✔
294
        // Update discovery version in status
2✔
295
        res.Status.PodGeneration = res.GetGeneration()
2✔
296
        if err := r.Status().Update(ctx, res); err != nil {
2✔
UNCOV
297
                return errLogAndWrap(log, err, "failed to update status")
×
UNCOV
298
        }
×
299

300
        return nil
2✔
301
}
302

303
func objectMeta(res *solarv1alpha1.Discovery) metav1.ObjectMeta {
6✔
304
        labels := res.Labels
6✔
305
        if labels == nil {
12✔
306
                labels = make(map[string]string)
6✔
307
        }
6✔
308
        labels["app.kubernetes.io/managed-by"] = "solar-discovery-controller"
6✔
309
        labels["app.kubernetes.io/component"] = "discovery-worker"
6✔
310
        labels["app.kubernetes.io/instance"] = res.Name
6✔
311
        labels["app.kubernetes.io/name"] = discoveryPrefixed(res.Name)
6✔
312

6✔
313
        return metav1.ObjectMeta{
6✔
314
                Name:        discoveryPrefixed(res.Name),
6✔
315
                Namespace:   res.Namespace,
6✔
316
                Labels:      labels,
6✔
317
                Annotations: res.Annotations,
6✔
318
        }
6✔
319
}
320

321
// discoveryPrefixed returns the name of the discovery prefixed resource
322
func discoveryPrefixed(discoveryName string) string {
27✔
323
        return fmt.Sprintf("discovery-%s", discoveryName)
27✔
324
}
27✔
325

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