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

opendefensecloud / solution-arsenal / 23485901803

24 Mar 2026 10:56AM UTC coverage: 71.716% (-0.007%) from 71.723%
23485901803

push

github

web-flow
fix(controller): filter reconciliation by namespace and fix EnvTest test errors (#323)

* fix(controller): add WatchNamespace field to reconcilers to fix EnvTest namespace errors

EnvTest does not support namespace deletion - namespaces are only marked for
termination. When running all controller tests together, reconcilers continue
reconciling objects from terminating namespaces, causing spurious errors.

This change adds a WatchNamespace field to all reconcilers. In tests, the field
is set per-test via BeforeEach/AfterEach to restrict reconciliation to only the
current test namespace. This prevents reconcilers from processing objects in
namespaces that are being cleaned up from previous tests.

The WatchNamespace field is empty by default (watches all namespaces), preserving
production behavior. This is intended for integration tests only.

See: https://book.kubebuilder.io/reference/envtest#testing-considerations

reduce diff-noise

* test: move auth secret creation to BeforeSuite

Move the rendertask-secret creation from BeforeAll in
rendertask_controller_test.go to BeforeSuite in suite_test.go.
This avoids duplicating the secret creation across test files and
ensures it's available for all controller tests.

* test(controller): suppress namespace terminating errors in errLogAndWrap

EnvTest marks namespaces for termination rather than deleting them, causing
reconcilers to fail with "unable to create new content in namespace because
it is being terminated" errors during test cleanup. These are not real bugs
but EnvTest limitations.

This change modifies errLogAndWrap to silently ignore errors containing
"because it is being terminated", suppressing the error noise while still
returning nil to allow reconciliation to complete.

See: https://book.kubebuilder.io/reference/envtest#testing-considerations

* chore: Consistent naming

* tests: Only suppress NamespaceTerminatingErrors while testing

* fix: isTestMode() instead of testMode var

The variable... (continued)

23 of 29 new or added lines in 6 files covered. (79.31%)

12 existing lines in 2 files now uncovered.

2178 of 3037 relevant lines covered (71.72%)

13.89 hits per line

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

66.51
/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
        rbacv1 "k8s.io/api/rbac/v1"
13
        apierrors "k8s.io/apimachinery/pkg/api/errors"
14
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15
        "k8s.io/apimachinery/pkg/runtime"
16
        "k8s.io/apimachinery/pkg/types"
17
        "k8s.io/apimachinery/pkg/util/intstr"
18
        "k8s.io/client-go/tools/events"
19
        ctrl "sigs.k8s.io/controller-runtime"
20
        "sigs.k8s.io/controller-runtime/pkg/client"
21
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
22

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

27
const (
28
        discoveryFinalizer = "solar.opendefense.cloud/discovery-finalizer"
29
        workerRoleName     = "solar-discovery-worker"
30
)
31

32
// DiscoveryReconciler reconciles a Discovery object
33
type DiscoveryReconciler struct {
34
        client.Client
35
        Scheme        *runtime.Scheme
36
        Recorder      events.EventRecorder
37
        WorkerImage   string
38
        WorkerCommand string
39
        WorkerArgs    []string
40
        // WatchNamespace restricts reconciliation to this namespace.
41
        // Should be empty in production (watches all namespaces).
42
        // Intended for use in integration tests only.
43
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
44
        WatchNamespace string
45
}
46

47
//nolint:lll
48
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries,verbs=get;list;watch;create;update;patch;delete
49
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries/status,verbs=get;update;patch
50
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=discoveries/finalizers,verbs=update
51
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
52
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
53
//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
54
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
55
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete
56
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch;delete
57
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
58
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
59
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=components,verbs=get;list;watch;create;update;patch;delete
60
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch;create;update;patch;delete
61

62
// needed in order to be able to grant permissions
63
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=components,verbs=get;list;watch;create;update;patch;delete
64
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=componentversions,verbs=get;list;watch;create;update;patch;delete
65

66
// Reconcile moves the current state of the cluster closer to the desired state
67
func (r *DiscoveryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
21✔
68
        log := ctrl.LoggerFrom(ctx)
21✔
69
        ctrlResult := ctrl.Result{}
21✔
70

21✔
71
        log.V(1).Info("Discovery is being reconciled", "req", req)
21✔
72

21✔
73
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
21✔
NEW
74
                return ctrlResult, nil
×
NEW
75
        }
×
76

77
        // Fetch the Order instance
78
        res := &solarv1alpha1.Discovery{}
21✔
79
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
22✔
80
                if apierrors.IsNotFound(err) {
2✔
81
                        // Object not found, return. Created objects are automatically garbage collected.
1✔
82
                        return ctrlResult, nil
1✔
83
                }
1✔
84

85
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
86
        }
87

88
        // Handle deletion: cleanup artifact workflows, then remove finalizer
89
        if !res.DeletionTimestamp.IsZero() {
22✔
90
                log.V(1).Info("Discovery is being deleted")
2✔
91
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "Discovery is being deleted, cleaning up worker")
2✔
92

2✔
93
                // Cleanup worker resources, if exists
2✔
94
                if err := r.deleteWorkerResources(ctx, res); err != nil {
2✔
95
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up worker resources")
×
96
                }
×
97

98
                // Remove finalizer
99
                if slices.Contains(res.Finalizers, discoveryFinalizer) {
4✔
100
                        log.V(1).Info("Removing finalizer from resource")
2✔
101
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
4✔
102
                                return f == discoveryFinalizer
2✔
103
                        })
2✔
104
                        if err := r.Update(ctx, res); err != nil {
3✔
105
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
1✔
106
                        }
1✔
107
                }
108

109
                return ctrlResult, nil
1✔
110
        }
111

112
        // Add finalizer if not present and not deleting
113
        if res.DeletionTimestamp.IsZero() {
36✔
114
                if !slices.Contains(res.Finalizers, discoveryFinalizer) {
22✔
115
                        log.V(1).Info("Adding finalizer to resource")
4✔
116
                        res.Finalizers = append(res.Finalizers, discoveryFinalizer)
4✔
117
                        if err := r.Update(ctx, res); err != nil {
4✔
118
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
119
                        }
×
120
                        // Return without requeue; the Update event will trigger reconciliation again
121
                        return ctrlResult, nil
4✔
122
                }
123
        }
124

125
        pod := &corev1.Pod{}
14✔
126
        err := r.Get(ctx, types.NamespacedName{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}, pod)
14✔
127
        if err != nil && !apierrors.IsNotFound(err) {
14✔
128
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodNotFound", "GetPod", "Failed to get pod", err)
×
129
                return ctrlResult, errLogAndWrap(log, err, "failed to get pod information")
×
130
        }
×
131

132
        // No pod yet, create it.
133
        if apierrors.IsNotFound(err) {
18✔
134
                if err := r.createWorkerResources(ctx, res); err != nil {
4✔
135
                        return ctrlResult, errLogAndWrap(log, err, "failed to create pod")
×
136
                }
×
137

138
                return ctrlResult, nil
4✔
139
        }
140

141
        // Pod exists, check if it's up to date with our configuration and if it is healthy.
142
        if res.Status.PodGeneration != res.GetGeneration() {
14✔
143
                // Recreate pod, configuration mismatch
4✔
144
                r.Recorder.Eventf(res, nil, corev1.EventTypeNormal, "ConfigurationChanged", "CompareConfiguration", "Configuration changed. Replacing pod.")
4✔
145
                if err := r.deleteWorkerResources(ctx, res); err != nil {
4✔
146
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up worker resources")
×
147
                }
×
148

149
                if err := r.createWorkerResources(ctx, res); err != nil {
6✔
150
                        return ctrlResult, errLogAndWrap(log, err, "failed to create pod")
2✔
151
                }
2✔
152

153
                return ctrlResult, nil
2✔
154
        } else {
6✔
155
                log.V(1).Info("Configuration hasn't changed", "podGen", res.Status.PodGeneration, "gen", res.GetGeneration())
6✔
156
        }
6✔
157

158
        return ctrlResult, nil
6✔
159
}
160

161
// deleteWorkerResources deletes the resources of the worker pod
162
func (r *DiscoveryReconciler) deleteWorkerResources(ctx context.Context, res *solarv1alpha1.Discovery) error {
6✔
163
        log := ctrl.LoggerFrom(ctx)
6✔
164

6✔
165
        if err := r.Delete(ctx, &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
6✔
166
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceDeletionFailed", "DeleteService", "Failed to delete service", err)
×
167
                return errLogAndWrap(log, err, "service deletion failed")
×
168
        }
×
169

170
        if err := r.Delete(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
6✔
171
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretDeletionFailed", "DeleteSecret", "Failed to delete secret", err)
×
172
                return errLogAndWrap(log, err, "secret deletion failed")
×
173
        }
×
174

175
        if err := r.Delete(ctx, &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: discoveryPrefixed(res.Name), Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
6✔
176
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodDeletionFailed", "DeletePod", "Failed to delete pod", err)
×
177
                return errLogAndWrap(log, err, "pod deletion failed")
×
178
        }
×
179

180
        if err := r.Delete(ctx, &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: workerRoleName, Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
6✔
181
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleDeletionFailed", "DeleteRole", "Failed to delete role", err)
×
182
                return errLogAndWrap(log, err, "role deletion failed")
×
183
        }
×
184

185
        if err := r.Delete(ctx, &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: workerRoleName, Namespace: res.Namespace}}); err != nil && !apierrors.IsNotFound(err) {
6✔
186
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleBindingDeletionFailed", "DeleteRoleBinding", "Failed to delete rolebinding", err)
×
187
                return errLogAndWrap(log, err, "rolebinding deletion failed")
×
188
        }
×
189

190
        return nil
6✔
191
}
192

193
// createWorkerResources creates the necessary resources for the worker pod
194
func (r *DiscoveryReconciler) createWorkerResources(ctx context.Context, res *solarv1alpha1.Discovery) error {
8✔
195
        log := ctrl.LoggerFrom(ctx)
8✔
196

8✔
197
        // Create or get service account in the discovery's namespace
8✔
198
        workerSA := &corev1.ServiceAccount{
8✔
199
                ObjectMeta: objectMeta(res),
8✔
200
        }
8✔
201

8✔
202
        existingSA := &corev1.ServiceAccount{}
8✔
203
        err := r.Get(ctx, types.NamespacedName{Name: workerSA.Name, Namespace: workerSA.Namespace}, existingSA)
8✔
204
        if err != nil && !apierrors.IsNotFound(err) {
8✔
205
                return errLogAndWrap(log, err, "failed to get service account")
×
206
        }
×
207

208
        if apierrors.IsNotFound(err) {
12✔
209
                if err := r.Create(ctx, workerSA); err != nil {
4✔
210
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceAccountCreationFailed", "CreateServiceAccount", "Failed to create service account", err)
×
211
                        return errLogAndWrap(log, err, "failed to create service account")
×
212
                }
×
213
                r.Recorder.Eventf(res, workerSA, corev1.EventTypeNormal, "ServiceAccountCreated", "CreateServiceAccount", "ServiceAccount created")
4✔
214
                if err := controllerutil.SetControllerReference(res, workerSA, r.Scheme); err != nil {
4✔
215
                        return errLogAndWrap(log, err, "failed to set controller reference on service account")
×
216
                }
×
217
        }
218

219
        // Create Role to define RBAC permissions required for discovery worker
220
        role := &rbacv1.Role{
8✔
221
                ObjectMeta: metav1.ObjectMeta{
8✔
222
                        Name:      workerRoleName,
8✔
223
                        Namespace: res.Namespace,
8✔
224
                        Labels: map[string]string{
8✔
225
                                "app.kubernetes.io/managed-by": "solar-discovery-controller",
8✔
226
                        },
8✔
227
                },
8✔
228
                Rules: []rbacv1.PolicyRule{
8✔
229
                        {
8✔
230
                                APIGroups: []string{solarv1alpha1.SchemeGroupVersion.Group},
8✔
231
                                Resources: []string{"componentversions", "components"},
8✔
232
                                Verbs:     []string{"get", "list", "watch", "create", "update", "patch", "delete"},
8✔
233
                        },
8✔
234
                },
8✔
235
        }
8✔
236

8✔
237
        existingRole := &rbacv1.Role{}
8✔
238
        err = r.Get(ctx, types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, existingRole)
8✔
239
        if err != nil && !apierrors.IsNotFound(err) {
8✔
240
                return errLogAndWrap(log, err, "failed to get role")
×
241
        }
×
242

243
        if apierrors.IsNotFound(err) {
16✔
244
                if err := r.Create(ctx, role); err != nil {
8✔
245
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleCreationFailed", "CreateRole", "Failed to create role", err)
×
246
                        return errLogAndWrap(log, err, "failed to create role")
×
247
                }
×
248
                r.Recorder.Eventf(res, role, corev1.EventTypeNormal, "RoleCreated", "CreateRole", "Role created")
8✔
249
                if err := controllerutil.SetControllerReference(res, role, r.Scheme); err != nil {
8✔
250
                        return errLogAndWrap(log, err, "failed to set controller reference on role")
×
251
                }
×
252
        } else {
×
253
                // check if out of sync
×
254
                needsUpdate := false
×
255
                if len(existingRole.Rules) != len(role.Rules) ||
×
256
                        !slices.Equal(existingRole.Rules[0].Verbs, role.Rules[0].Verbs) ||
×
257
                        !slices.Equal(existingRole.Rules[0].APIGroups, role.Rules[0].APIGroups) ||
×
258
                        !slices.Equal(existingRole.Rules[0].Resources, role.Rules[0].Resources) {
×
259
                        existingRole.Rules = role.Rules
×
260
                        needsUpdate = true
×
261
                }
×
262
                if needsUpdate {
×
263
                        if err := r.Update(ctx, existingRole); err != nil {
×
264
                                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleUpdateFailed", "UpdateRole", "Failed to update role", err)
×
265
                                return errLogAndWrap(log, err, "failed to update role")
×
266
                        }
×
267
                        r.Recorder.Eventf(res, existingRole, corev1.EventTypeNormal, "RoleUpdated", "UpdateRole", "Role updated")
×
268
                }
269
        }
270

271
        // Create roleBinding to grant RBAC permissions to the worker service account
272
        roleBinding := &rbacv1.RoleBinding{
8✔
273
                ObjectMeta: metav1.ObjectMeta{
8✔
274
                        Name:      workerRoleName,
8✔
275
                        Namespace: res.Namespace,
8✔
276
                        Labels: map[string]string{
8✔
277
                                "app.kubernetes.io/managed-by": "solar-discovery-controller",
8✔
278
                        },
8✔
279
                },
8✔
280
                RoleRef: rbacv1.RoleRef{
8✔
281
                        APIGroup: "rbac.authorization.k8s.io",
8✔
282
                        Kind:     "Role",
8✔
283
                        Name:     workerRoleName,
8✔
284
                },
8✔
285
                Subjects: []rbacv1.Subject{
8✔
286
                        {
8✔
287
                                Kind:      "ServiceAccount",
8✔
288
                                Name:      workerSA.Name,
8✔
289
                                Namespace: res.Namespace,
8✔
290
                        },
8✔
291
                },
8✔
292
        }
8✔
293

8✔
294
        existingRB := &rbacv1.RoleBinding{}
8✔
295
        err = r.Get(ctx, types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, existingRB)
8✔
296
        if err != nil && !apierrors.IsNotFound(err) {
8✔
297
                return errLogAndWrap(log, err, "failed to get rolebinding")
×
298
        }
×
299

300
        if apierrors.IsNotFound(err) {
16✔
301
                if err := r.Create(ctx, roleBinding); err != nil {
8✔
302
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleBindingCreationFailed", "CreateRoleBinding", "Failed to create rolebinding", err)
×
303
                        return errLogAndWrap(log, err, "failed to create rolebinding")
×
304
                }
×
305
                r.Recorder.Eventf(res, roleBinding, corev1.EventTypeNormal, "RoleBindingCreated", "CreateRoleBinding", "RoleBinding created")
8✔
306
                if err := controllerutil.SetControllerReference(res, roleBinding, r.Scheme); err != nil {
8✔
307
                        return errLogAndWrap(log, err, "failed to set controller reference on rolebinding")
×
308
                }
×
309
        } else {
×
310
                needsUpdate := false
×
311
                if existingRB.RoleRef.Name != workerRoleName {
×
312
                        existingRB.RoleRef.Name = workerRoleName
×
313
                        needsUpdate = true
×
314
                }
×
315
                if len(existingRB.Subjects) != 1 ||
×
316
                        existingRB.Subjects[0].Kind != "ServiceAccount" ||
×
317
                        existingRB.Subjects[0].Name != discoveryPrefixed(res.Name) ||
×
318
                        existingRB.Subjects[0].Namespace != res.Namespace {
×
319
                        existingRB.Subjects = roleBinding.Subjects
×
320
                        needsUpdate = true
×
321
                }
×
322

323
                if needsUpdate {
×
324
                        if err := r.Update(ctx, existingRB); err != nil {
×
325
                                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "RoleBindingUpdateFailed", "UpdateRoleBinding", "Failed to update rolebinding", err)
×
326
                                return errLogAndWrap(log, err, "failed to update rolebinding")
×
327
                        }
×
328
                        r.Recorder.Eventf(res, existingRB, corev1.EventTypeNormal, "RoleBindingUpdated", "UpdateRoleBinding", "RoleBinding updated")
×
329
                }
330
        }
331

332
        // Create secret
333
        secret := &corev1.Secret{
8✔
334
                ObjectMeta: objectMeta(res),
8✔
335
        }
8✔
336

8✔
337
        rp := discovery.NewRegistryProvider()
8✔
338
        reg := &discovery.Registry{
8✔
339
                Name:      res.Name,
8✔
340
                PlainHTTP: res.Spec.Registry.PlainHTTP,
8✔
341
                Hostname:  res.Spec.Registry.RegistryURL,
8✔
342
        }
8✔
343
        if res.Spec.Webhook != nil {
8✔
344
                reg.WebhookPath = res.Spec.Webhook.Path
×
345
                reg.Flavor = res.Spec.Webhook.Flavor
×
346
        }
×
347
        if res.Spec.DiscoveryInterval != nil {
13✔
348
                reg.ScanInterval = res.Spec.DiscoveryInterval.Duration
5✔
349
        }
5✔
350
        if err := rp.Register(reg); err != nil {
8✔
351
                return errLogAndWrap(log, err, "failed to register registry")
×
352
        }
×
353

354
        // Add credentials if specified
355
        if res.Spec.Registry.SecretRef.Name != "" {
8✔
356
                sec := &corev1.Secret{}
×
357
                if err := r.Get(ctx, types.NamespacedName{Name: res.Spec.Registry.SecretRef.Name, Namespace: res.Namespace}, sec); err != nil {
×
358
                        return errLogAndWrap(log, err, "failed to get registry secret")
×
359
                } else {
×
360
                        username, okUser := sec.Data["username"]
×
361
                        password, okPass := sec.Data["password"]
×
362
                        if okUser && okPass {
×
363
                                reg.Credentials = &discovery.RegistryCredentials{
×
364
                                        Username: string(username),
×
365
                                        Password: string(password),
×
366
                                }
×
367
                        } else {
×
368
                                return fmt.Errorf("registry secret is missing username or password fields")
×
369
                        }
×
370
                }
371
        }
372

373
        confData, err := rp.Marshal()
8✔
374
        if err != nil {
8✔
375
                return errLogAndWrap(log, err, "failed to marshal registry configuration")
×
376
        }
×
377
        secret.StringData = map[string]string{
8✔
378
                "config.yaml": string(confData),
8✔
379
        }
8✔
380

8✔
381
        existingSecret := &corev1.Secret{}
8✔
382
        err = r.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, existingSecret)
8✔
383
        if err != nil && !apierrors.IsNotFound(err) {
8✔
384
                return errLogAndWrap(log, err, "failed to get secret")
×
385
        }
×
386

387
        if apierrors.IsNotFound(err) {
16✔
388
                if err := r.Create(ctx, secret); err != nil {
8✔
389
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretCreationFailed", "CreateSecret", "Failed to create secret", err)
×
390
                        return errLogAndWrap(log, err, "failed to create secret")
×
391
                }
×
392
                r.Recorder.Eventf(res, secret, corev1.EventTypeNormal, "SecretCreated", "CreateSecret", "Secret created")
8✔
393

8✔
394
                if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
8✔
395
                        return errLogAndWrap(log, err, "failed to set controller reference")
×
396
                }
×
397
        } else {
×
398
                existingSecret.StringData = secret.StringData
×
399
                if err := r.Update(ctx, existingSecret); err != nil {
×
400
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "SecretUpdateFailed", "UpdateSecret", "Failed to update secret", err)
×
401
                        return errLogAndWrap(log, err, "failed to update secret")
×
402
                }
×
403
                r.Recorder.Eventf(res, existingSecret, corev1.EventTypeNormal, "SecretUpdated", "UpdateSecret", "Secret updated")
×
404

×
405
                if err := controllerutil.SetControllerReference(res, existingSecret, r.Scheme); err != nil {
×
406
                        return errLogAndWrap(log, err, "failed to set controller reference")
×
407
                }
×
408
        }
409

410
        // Create pod
411
        var args = r.WorkerArgs
8✔
412
        args = append(args, "--config", "/etc/worker/config.yaml", "--namespace", res.Namespace)
8✔
413
        pod := &corev1.Pod{
8✔
414
                ObjectMeta: objectMeta(res),
8✔
415
                Spec: corev1.PodSpec{
8✔
416
                        ServiceAccountName: workerSA.Name,
8✔
417
                        Volumes: []corev1.Volume{
8✔
418
                                {
8✔
419
                                        Name: "config",
8✔
420
                                        VolumeSource: corev1.VolumeSource{
8✔
421
                                                Secret: &corev1.SecretVolumeSource{
8✔
422
                                                        SecretName: discoveryPrefixed(res.Name),
8✔
423
                                                },
8✔
424
                                        },
8✔
425
                                },
8✔
426
                        },
8✔
427
                },
8✔
428
        }
8✔
429

8✔
430
        container := corev1.Container{
8✔
431

8✔
432
                Name:    "worker",
8✔
433
                Image:   r.WorkerImage,
8✔
434
                Command: []string{r.WorkerCommand},
8✔
435
                Args:    args,
8✔
436
                VolumeMounts: []corev1.VolumeMount{
8✔
437
                        {
8✔
438
                                Name:      "config",
8✔
439
                                ReadOnly:  true,
8✔
440
                                MountPath: "/etc/worker"},
8✔
441
                },
8✔
442
                Ports: []corev1.ContainerPort{
8✔
443
                        {
8✔
444
                                Name:          "webhook",
8✔
445
                                ContainerPort: 8080,
8✔
446
                        },
8✔
447
                },
8✔
448
        }
8✔
449

8✔
450
        if cmName := res.Spec.Registry.CAConfigMapRef.Name; cmName != "" {
10✔
451
                pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
2✔
452
                        Name: "ca-bundle",
2✔
453
                        VolumeSource: corev1.VolumeSource{
2✔
454
                                ConfigMap: &corev1.ConfigMapVolumeSource{
2✔
455
                                        LocalObjectReference: corev1.LocalObjectReference{
2✔
456
                                                Name: cmName,
2✔
457
                                        },
2✔
458
                                        Items: []corev1.KeyToPath{
2✔
459
                                                {
2✔
460
                                                        Key:  "trust-bundle.pem",
2✔
461
                                                        Path: "ca-bundle.pem",
2✔
462
                                                },
2✔
463
                                        },
2✔
464
                                },
2✔
465
                        },
2✔
466
                })
2✔
467
                container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
2✔
468
                        Name:      "ca-bundle",
2✔
469
                        MountPath: "/etc/ssl/certs",
2✔
470
                        ReadOnly:  true,
2✔
471
                })
2✔
472
                container.Env = append(container.Env, corev1.EnvVar{
2✔
473
                        Name:  "SSL_CERT_FILE",
2✔
474
                        Value: "/etc/ssl/certs/ca-bundle.pem",
2✔
475
                })
2✔
476
        }
2✔
477

478
        pod.Spec.Containers = []corev1.Container{container}
8✔
479

8✔
480
        // Set owner references
8✔
481
        if err := controllerutil.SetControllerReference(res, pod, r.Scheme); err != nil {
8✔
482
                return errLogAndWrap(log, err, "failed to set controller reference")
×
483
        }
×
484

485
        // Create pod in cluster
486
        if err := r.Create(ctx, pod); err != nil {
8✔
487
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "PodCreationFailed", "CreatePod", "Failed to create pod", err)
×
488
                return errLogAndWrap(log, err, "failed to create pod")
×
489
        }
×
490
        r.Recorder.Eventf(res, pod, corev1.EventTypeNormal, "PodCreated", "CreatePod", "Pod created")
8✔
491
        log.V(1).Info("Pod created", "podGen", res.GetGeneration())
8✔
492

8✔
493
        // Create or update service
8✔
494
        svc := &corev1.Service{
8✔
495
                ObjectMeta: objectMeta(res),
8✔
496
                Spec: corev1.ServiceSpec{
8✔
497
                        Type:     corev1.ServiceTypeClusterIP,
8✔
498
                        Ports:    []corev1.ServicePort{{Name: "webhook", Port: 8080, TargetPort: intstr.FromString("webhook")}},
8✔
499
                        Selector: map[string]string{"app.kubernetes.io/name": discoveryPrefixed(res.Name)},
8✔
500
                },
8✔
501
        }
8✔
502

8✔
503
        existingSvc := &corev1.Service{}
8✔
504
        err = r.Get(ctx, types.NamespacedName{Name: svc.Name, Namespace: svc.Namespace}, existingSvc)
8✔
505
        if err != nil && !apierrors.IsNotFound(err) {
8✔
506
                return errLogAndWrap(log, err, "failed to get service")
×
507
        }
×
508

509
        if apierrors.IsNotFound(err) {
16✔
510
                if err := r.Create(ctx, svc); err != nil {
8✔
511
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceCreationFailed", "CreateService", "Failed to create service", err)
×
512
                        return errLogAndWrap(log, err, "failed to create service")
×
513
                }
×
514
                r.Recorder.Eventf(res, svc, corev1.EventTypeNormal, "ServiceCreated", "CreateService", "Service created")
8✔
515
        } else {
×
516
                existingSvc.Spec = svc.Spec
×
517
                if err := r.Update(ctx, existingSvc); err != nil {
×
518
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "ServiceUpdateFailed", "UpdateService", "Failed to update service", err)
×
519
                        return errLogAndWrap(log, err, "failed to update service")
×
520
                }
×
521
                r.Recorder.Eventf(res, existingSvc, corev1.EventTypeNormal, "ServiceUpdated", "UpdateService", "Service updated")
×
522
        }
523

524
        // Update discovery version in status
525
        res.Status.PodGeneration = res.GetGeneration()
8✔
526
        if err := r.Status().Update(ctx, res); err != nil {
10✔
527
                return errLogAndWrap(log, err, "failed to update status")
2✔
528
        }
2✔
529

530
        return nil
6✔
531
}
532

533
func objectMeta(res *solarv1alpha1.Discovery) metav1.ObjectMeta {
32✔
534
        labels := res.Labels
32✔
535
        if labels == nil {
64✔
536
                labels = make(map[string]string)
32✔
537
        }
32✔
538
        labels["app.kubernetes.io/managed-by"] = "solar-discovery-controller"
32✔
539
        labels["app.kubernetes.io/component"] = "discovery-worker"
32✔
540
        labels["app.kubernetes.io/instance"] = res.Name
32✔
541
        labels["app.kubernetes.io/name"] = discoveryPrefixed(res.Name)
32✔
542

32✔
543
        return metav1.ObjectMeta{
32✔
544
                Name:        discoveryPrefixed(res.Name),
32✔
545
                Namespace:   res.Namespace,
32✔
546
                Labels:      labels,
32✔
547
                Annotations: res.Annotations,
32✔
548
        }
32✔
549
}
550

551
// discoveryPrefixed returns the name of the discovery prefixed resource
552
func discoveryPrefixed(discoveryName string) string {
130✔
553
        return fmt.Sprintf("discovery-%s", discoveryName)
130✔
554
}
130✔
555

556
// SetupWithManager sets up the controller with the Manager.
557
func (r *DiscoveryReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
558
        return ctrl.NewControllerManagedBy(mgr).
1✔
559
                For(&solarv1alpha1.Discovery{}).
1✔
560
                Owns(&corev1.Pod{}).
1✔
561
                Owns(&corev1.Secret{}).
1✔
562
                Complete(r)
1✔
563
}
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