• 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

87.55
/pkg/controller/rendertask_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
        "encoding/json"
9
        "fmt"
10
        "slices"
11
        "strings"
12
        "time"
13

14
        batchv1 "k8s.io/api/batch/v1"
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
        renderTaskFinalizer = "solar.opendefense.cloud/rendertask-finalizer"
30

31
        annotationJobName    = "solar.opendefense.cloud/job-name"
32
        annotationSecretName = "solar.opendefense.cloud/secret-name"
33

34
        // Condition types
35
        ConditionTypeJobScheduled = "JobScheduled"
36
        ConditionTypeJobSucceeded = "JobSucceeded"
37
        ConditionTypeJobFailed    = "JobFailed"
38

39
        ConditionTypeTaskCompleted = "TaskCompleted"
40
        ConditionTypeTaskFailed    = "TaskFailed"
41
)
42

43
// RenderTaskReconciler reconciles a RenderTask object
44
type RenderTaskReconciler struct {
45
        client.Client
46
        Scheme              *runtime.Scheme
47
        Recorder            events.EventRecorder
48
        RendererImage       string
49
        RendererCommand     string
50
        RendererArgs        []string
51
        PushSecretRef       *corev1.SecretReference
52
        BaseURL             string
53
        RendererCAConfigMap string
54
        // WatchNamespace restricts reconciliation to this namespace.
55
        // Should be empty in production (watches all namespaces).
56
        // Intended for use in integration tests only.
57
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
58
        WatchNamespace string
59
}
60

61
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks,verbs=get;list;watch;create;update;patch;delete
62
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks/status,verbs=get;update;patch
63
//+kubebuilder:rbac:groups=solar.opendefense.cloud,resources=rendertasks/finalizers,verbs=update
64
//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
65
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
66
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
67
//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
68

69
// Reconcile moves the current state of the cluster closer to the desired state
70
func (r *RenderTaskReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
203✔
71
        log := ctrl.LoggerFrom(ctx)
203✔
72
        ctrlResult := ctrl.Result{}
203✔
73

203✔
74
        log.V(1).Info("RenderTask is being reconciled", "req", req)
203✔
75

203✔
76
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
232✔
77
                return ctrlResult, nil
29✔
78
        }
29✔
79

80
        // Fetch the RenderTask instance
81
        res := &solarv1alpha1.RenderTask{}
174✔
82
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
183✔
83
                if apierrors.IsNotFound(err) {
18✔
84
                        // Object not found, return. Created objects are automatically garbage collected.
9✔
85
                        return ctrlResult, nil
9✔
86
                }
9✔
87

88
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
89
        }
90

91
        // Handle deletion: cleanup job and secret, then remove finalizer
92
        if !res.DeletionTimestamp.IsZero() {
179✔
93
                log.V(1).Info("RenderTask is being deleted")
14✔
94
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "RenderTask is being deleted, cleaning up secret and job")
14✔
95

14✔
96
                // Cleanup render resources, if exists
14✔
97
                if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
14✔
98
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up render job")
×
99
                }
×
100

101
                if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
14✔
102
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up render secret")
×
103
                }
×
104

105
                if err := r.deleteAuthSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
14✔
106
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up auth secret")
×
107
                }
×
108

109
                // Remove finalizer
110
                if slices.Contains(res.Finalizers, renderTaskFinalizer) {
28✔
111
                        log.V(1).Info("Removing finalizer from resource")
14✔
112
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
28✔
113
                                return f == renderTaskFinalizer
14✔
114
                        })
14✔
115
                        if err := r.Update(ctx, res); err != nil {
17✔
116
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
3✔
117
                        }
3✔
118
                }
119

120
                return ctrlResult, nil
11✔
121
        }
122

123
        // Add finalizer if not present and not deleting
124
        if res.DeletionTimestamp.IsZero() {
302✔
125
                if !slices.Contains(res.Finalizers, renderTaskFinalizer) {
193✔
126
                        log.V(1).Info("Adding finalizer to resource")
42✔
127
                        res.Finalizers = append(res.Finalizers, renderTaskFinalizer)
42✔
128
                        if err := r.Update(ctx, res); err != nil {
42✔
129
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
×
130
                        }
×
131
                        // Return without requeue; the Update event will trigger reconciliation again
132
                        return ctrlResult, nil
42✔
133
                }
134
        }
135

136
        // Check if renderjob has already completed successfully
137
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
109✔
138
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
113✔
139
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
4✔
140
                return ctrlResult, nil
4✔
141
        }
4✔
142

143
        // Check if renderjob has already failed
144
        fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
105✔
145
        if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue {
108✔
146
                log.V(1).Info("RenderTask has already failed, no further action needed")
3✔
147
                return ctrlResult, nil
3✔
148
        }
3✔
149

150
        // Reconcile Config Secret
151
        configSecret := &corev1.Secret{}
102✔
152
        err := r.Get(ctx, r.configSecretKey(res), configSecret)
102✔
153
        if err != nil && apierrors.IsNotFound(err) {
144✔
154
                createdSecret, err := r.createConfigSecret(ctx, res)
42✔
155
                if err != nil {
42✔
UNCOV
156
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", fmt.Sprintf("Failed to create config secret: %s", err))
×
UNCOV
157
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
×
UNCOV
158
                }
×
159
                configSecret = createdSecret
42✔
160
        } else if err != nil {
60✔
161
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
162
        }
×
163

164
        // Reconcile Auth Secret
165
        authSecret := &corev1.Secret{}
102✔
166
        err = r.Get(ctx, r.authSecretKey(res), authSecret)
102✔
167
        if err != nil && apierrors.IsNotFound(err) && r.PushSecretRef != nil {
144✔
168
                createdSecret, err := r.copyAuthSecret(ctx, res)
42✔
169
                if err != nil {
42✔
UNCOV
170
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateAuthSecret", fmt.Sprintf("Failed to create auth secret: %s", err))
×
UNCOV
171
                        return ctrlResult, errLogAndWrap(log, err, "failed to copy auth secret to namespace")
×
UNCOV
172
                }
×
173
                authSecret = createdSecret
42✔
174
        } else if client.IgnoreNotFound(err) != nil {
60✔
175
                return ctrlResult, errLogAndWrap(log, err, "could not get auth secret")
×
176
        }
×
177

178
        // Reconcile Job
179
        job := &batchv1.Job{}
102✔
180
        err = r.Get(ctx, r.renderJobKey(res), job)
102✔
181
        if err != nil && apierrors.IsNotFound(err) {
144✔
182
                err := r.createRenderJob(ctx, res, configSecret, authSecret)
42✔
183
                if err != nil {
42✔
184
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", fmt.Sprintf("Failed to create job: %s", err))
×
185
                        return ctrlResult, errLogAndWrap(log, err, "failed to create job")
×
186
                }
×
187
        } else if err != nil {
60✔
188
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
189
        }
×
190

191
        // Update Status
192
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
160✔
193
                if err := r.Status().Update(ctx, res); err != nil {
70✔
194
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
12✔
195
                }
12✔
196
        }
197

198
        // Check if we need to clean up
199
        if isJobComplete(job) && job.Status.Succeeded > 0 {
93✔
200
                if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
201
                        r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job", err)
×
202
                        return ctrlResult, nil
×
203
                }
×
204
                if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
205
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret", err)
×
206
                        return ctrlResult, nil
×
207
                }
×
208
                if err := r.deleteAuthSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
209
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete auth secret", err)
×
210
                        return ctrlResult, nil
×
211
                }
×
212
                log.V(1).Info("Cleaned up after successful job")
3✔
213

3✔
214
                return ctrlResult, nil
3✔
215
        }
216

217
        // Check if job is still running
218
        if !isJobComplete(job) {
174✔
219
                log.V(1).Info("Job is still running, requeue after 5 seconds")
87✔
220
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
87✔
221
        }
87✔
222

223
        return ctrlResult, nil
×
224
}
225

226
// updateResourceStatusFromJob updates the resource status based on job status
227
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
102✔
228
        log := ctrl.LoggerFrom(ctx)
102✔
229

102✔
230
        if job == nil {
102✔
231
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
232
                        Type:               ConditionTypeJobScheduled,
×
233
                        Status:             metav1.ConditionFalse,
×
234
                        ObservedGeneration: res.Generation,
×
235
                        Reason:             "DoesNotExist",
×
236
                        Message:            "Renderer job does not exist",
×
237
                })
×
238

×
239
                return changed
×
240
        }
×
241

242
        if job.Status.Succeeded > 0 {
105✔
243
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3✔
244
                        Type:               ConditionTypeJobSucceeded,
3✔
245
                        Status:             metav1.ConditionTrue,
3✔
246
                        ObservedGeneration: res.Generation,
3✔
247
                        Reason:             "JobSucceeded",
3✔
248
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3✔
249
                })
3✔
250

3✔
251
                if res.Status.ChartURL != r.referenceURL(res.Spec.Repository, res.Spec.Tag) {
6✔
252
                        res.Status.ChartURL = r.referenceURL(res.Spec.Repository, res.Spec.Tag)
3✔
253
                        changed = true
3✔
254
                }
3✔
255

256
                r.Recorder.Eventf(res, job, corev1.EventTypeNormal, "JobSucceeded", "RunJob", "Renderer job completed successfully")
3✔
257
                log.V(1).Info("Job succeeded", "name", job.Name)
3✔
258

3✔
259
                return changed
3✔
260
        }
261

262
        if job.Status.Failed > 0 {
100✔
263
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
1✔
264
                        Type:               ConditionTypeJobFailed,
1✔
265
                        Status:             metav1.ConditionTrue,
1✔
266
                        ObservedGeneration: res.Generation,
1✔
267
                        Reason:             "JobFailed",
1✔
268
                        Message:            "Renderer job failed",
1✔
269
                })
1✔
270
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
1✔
271
                log.V(1).Info("Job failed", "name", job.Name)
1✔
272

1✔
273
                return changed
1✔
274
        }
1✔
275

276
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
98✔
277
                Type:               ConditionTypeJobScheduled,
98✔
278
                Status:             metav1.ConditionTrue,
98✔
279
                ObservedGeneration: res.Generation,
98✔
280
                Reason:             "JobScheduled",
98✔
281
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
98✔
282
        })
98✔
283
}
284

285
func (r *RenderTaskReconciler) deleteAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
17✔
286
        secret := &corev1.Secret{}
17✔
287
        if err := r.Get(ctx, r.authSecretKey(res), secret); err != nil {
20✔
288
                return err
3✔
289
        }
3✔
290

291
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
14✔
292
}
293

294
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask) error {
17✔
295
        job := &batchv1.Job{}
17✔
296
        if err := r.Get(ctx, r.renderJobKey(res), job); err != nil {
20✔
297
                return err
3✔
298
        }
3✔
299

300
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
14✔
301
}
302

303
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
17✔
304
        secret := &corev1.Secret{}
17✔
305
        if err := r.Get(ctx, r.configSecretKey(res), secret); err != nil {
20✔
306
                return err
3✔
307
        }
3✔
308

309
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
14✔
310
}
311

312
func (r *RenderTaskReconciler) copyAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
42✔
313
        log := ctrl.LoggerFrom(ctx)
42✔
314

42✔
315
        controllerSecret := &corev1.Secret{}
42✔
316
        if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, controllerSecret); err != nil {
42✔
UNCOV
317
                return nil, err
×
UNCOV
318
        }
×
319

320
        authSecret := &corev1.Secret{
42✔
321
                ObjectMeta: metav1.ObjectMeta{
42✔
322
                        Name:      r.authSecretKey(res).Name,
42✔
323
                        Namespace: r.authSecretKey(res).Namespace,
42✔
324
                },
42✔
325
                Type:       controllerSecret.Type,
42✔
326
                Data:       controllerSecret.Data,
42✔
327
                StringData: controllerSecret.StringData,
42✔
328
        }
42✔
329

42✔
330
        if err := r.Create(ctx, authSecret); err != nil {
43✔
331
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
1✔
332
                return nil, errLogAndWrap(log, err, "secret creation failed")
1✔
333
        }
1✔
334

335
        // Set owner references
336
        if err := controllerutil.SetControllerReference(res, authSecret, r.Scheme); err != nil {
41✔
337
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
338
        }
×
339

340
        return authSecret, nil
41✔
341
}
342

343
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, authSecret *corev1.Secret) error {
42✔
344
        log := ctrl.LoggerFrom(ctx)
42✔
345

42✔
346
        jobName := r.renderJobKey(res).Name
42✔
347
        backoffLimit := int32(3)
42✔
348
        ttlSecondsAfterFinished := int32(3600) // Clean up after 1 hour
42✔
349

42✔
350
        volumes := []corev1.Volume{
42✔
351
                {
42✔
352
                        Name: "config",
42✔
353
                        VolumeSource: corev1.VolumeSource{
42✔
354
                                Secret: &corev1.SecretVolumeSource{
42✔
355
                                        SecretName: configSecret.Name,
42✔
356
                                        Items: []corev1.KeyToPath{
42✔
357
                                                {
42✔
358
                                                        Key:  "config.json",
42✔
359
                                                        Path: "config.json",
42✔
360
                                                },
42✔
361
                                        },
42✔
362
                                },
42✔
363
                        },
42✔
364
                },
42✔
365
        }
42✔
366
        volumeMounts := []corev1.VolumeMount{
42✔
367
                {
42✔
368
                        Name:      "config",
42✔
369
                        MountPath: "/etc/renderer/config.json",
42✔
370
                        SubPath:   "config.json",
42✔
371
                        ReadOnly:  true,
42✔
372
                },
42✔
373
        }
42✔
374
        envVars := []corev1.EnvVar{
42✔
375
                {
42✔
376
                        Name: "POD_NAMESPACE",
42✔
377
                        ValueFrom: &corev1.EnvVarSource{
42✔
378
                                FieldRef: &corev1.ObjectFieldSelector{
42✔
379
                                        FieldPath: "metadata.namespace",
42✔
380
                                },
42✔
381
                        },
42✔
382
                },
42✔
383
                {
42✔
384
                        Name: "POD_NAME",
42✔
385
                        ValueFrom: &corev1.EnvVarSource{
42✔
386
                                FieldRef: &corev1.ObjectFieldSelector{
42✔
387
                                        FieldPath: "metadata.name",
42✔
388
                                },
42✔
389
                        },
42✔
390
                },
42✔
391
        }
42✔
392

42✔
393
        if r.RendererCAConfigMap != "" {
84✔
394
                volumes = append(volumes, corev1.Volume{
42✔
395
                        Name: "ca-bundle",
42✔
396
                        VolumeSource: corev1.VolumeSource{
42✔
397
                                ConfigMap: &corev1.ConfigMapVolumeSource{
42✔
398
                                        LocalObjectReference: corev1.LocalObjectReference{
42✔
399
                                                Name: r.RendererCAConfigMap,
42✔
400
                                        },
42✔
401
                                        Items: []corev1.KeyToPath{
42✔
402
                                                {
42✔
403
                                                        Key:  "trust-bundle.pem",
42✔
404
                                                        Path: "ca-bundle.pem",
42✔
405
                                                },
42✔
406
                                        },
42✔
407
                                },
42✔
408
                        },
42✔
409
                })
42✔
410
                volumeMounts = append(volumeMounts, corev1.VolumeMount{
42✔
411
                        Name:      "ca-bundle",
42✔
412
                        MountPath: "/etc/ssl/certs",
42✔
413
                        ReadOnly:  true,
42✔
414
                })
42✔
415
                envVars = append(envVars, corev1.EnvVar{
42✔
416
                        Name:  "SSL_CERT_FILE",
42✔
417
                        Value: "/etc/ssl/certs/ca-bundle.pem",
42✔
418
                })
42✔
419
        }
42✔
420

421
        job := &batchv1.Job{
42✔
422
                ObjectMeta: metav1.ObjectMeta{
42✔
423
                        Name:      jobName,
42✔
424
                        Namespace: r.renderJobKey(res).Namespace,
42✔
425
                        OwnerReferences: []metav1.OwnerReference{
42✔
426
                                {
42✔
427
                                        APIVersion:         solarv1alpha1.SchemeGroupVersion.String(),
42✔
428
                                        Kind:               res.Kind,
42✔
429
                                        Name:               res.Name,
42✔
430
                                        UID:                res.GetUID(),
42✔
431
                                        Controller:         new(true),
42✔
432
                                        BlockOwnerDeletion: new(true),
42✔
433
                                },
42✔
434
                        },
42✔
435
                        Annotations: map[string]string{
42✔
436
                                annotationJobName: jobName,
42✔
437
                        },
42✔
438
                },
42✔
439
                Spec: batchv1.JobSpec{
42✔
440
                        BackoffLimit:            &backoffLimit,
42✔
441
                        TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
42✔
442
                        Template: corev1.PodTemplateSpec{
42✔
443
                                Spec: corev1.PodSpec{
42✔
444
                                        RestartPolicy: corev1.RestartPolicyNever,
42✔
445
                                        Containers: []corev1.Container{
42✔
446
                                                {
42✔
447
                                                        Name:    "renderer",
42✔
448
                                                        Image:   r.RendererImage,
42✔
449
                                                        Command: []string{r.RendererCommand},
42✔
450
                                                        Args: append(r.RendererArgs,
42✔
451
                                                                "/etc/renderer/config.json",
42✔
452
                                                                fmt.Sprintf("--url=%s", r.referenceURL(res.Spec.Repository, res.Spec.Tag)),
42✔
453
                                                        ),
42✔
454
                                                        Env:          envVars,
42✔
455
                                                        VolumeMounts: volumeMounts,
42✔
456
                                                },
42✔
457
                                        },
42✔
458
                                        Volumes: volumes,
42✔
459
                                },
42✔
460
                        },
42✔
461
                },
42✔
462
        }
42✔
463

42✔
464
        if authSecret != nil {
83✔
465
                switch authSecret.Type {
41✔
466
                case corev1.SecretTypeBasicAuth:
1✔
467
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1✔
468
                                corev1.EnvVar{
1✔
469
                                        Name: "REGISTRY_USERNAME",
1✔
470
                                        ValueFrom: &corev1.EnvVarSource{
1✔
471
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
472
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
473
                                                                Name: authSecret.Name,
1✔
474
                                                        },
1✔
475
                                                        Key: "username",
1✔
476
                                                },
1✔
477
                                        },
1✔
478
                                },
1✔
479
                                corev1.EnvVar{
1✔
480
                                        Name: "REGISTRY_PASSWORD",
1✔
481
                                        ValueFrom: &corev1.EnvVarSource{
1✔
482
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
483
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
484
                                                                Name: authSecret.Name,
1✔
485
                                                        },
1✔
486
                                                        Key: "password",
1✔
487
                                                },
1✔
488
                                        },
1✔
489
                                },
1✔
490
                        )
1✔
491

492
                case corev1.SecretTypeDockerConfigJson:
16✔
493
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
16✔
494
                                Name: "dockerconfig",
16✔
495
                                VolumeSource: corev1.VolumeSource{
16✔
496
                                        Secret: &corev1.SecretVolumeSource{
16✔
497
                                                SecretName: authSecret.Name,
16✔
498
                                                Items: []corev1.KeyToPath{
16✔
499
                                                        {
16✔
500
                                                                Key:  ".dockerconfigjson",
16✔
501
                                                                Path: "dockerconfig.json",
16✔
502
                                                        },
16✔
503
                                                },
16✔
504
                                        },
16✔
505
                                },
16✔
506
                        })
16✔
507

16✔
508
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
16✔
509
                                Name:      "dockerconfig",
16✔
510
                                MountPath: "/etc/renderer/dockerconfig.json",
16✔
511
                                SubPath:   "dockerconfig.json",
16✔
512
                                ReadOnly:  true,
16✔
513
                        })
16✔
514

16✔
515
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
16✔
516
                                Name:  "DOCKER_CONFIG",
16✔
517
                                Value: "/etc/renderer/dockerconfig.json",
16✔
518
                        })
16✔
519
                default:
24✔
520
                }
521
        }
522

523
        if err := r.Create(ctx, job); err != nil {
43✔
524
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
1✔
525
                return errLogAndWrap(log, err, "job creation failed")
1✔
526
        }
1✔
527

528
        // Set owner references
529
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
41✔
530
                return errLogAndWrap(log, err, "failed to set controller reference")
×
531
        }
×
532

533
        res.Status.JobRef = &corev1.ObjectReference{
41✔
534
                APIVersion: batchv1.SchemeGroupVersion.String(),
41✔
535
                Kind:       "Job",
41✔
536
                Namespace:  job.Namespace,
41✔
537
                Name:       job.Name,
41✔
538
        }
41✔
539

41✔
540
        if err := r.Status().Update(ctx, res); err != nil {
41✔
541
                return errLogAndWrap(log, err, "failed to update status")
×
542
        }
×
543

544
        return nil
41✔
545
}
546

547
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
42✔
548
        log := ctrl.LoggerFrom(ctx)
42✔
549

42✔
550
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
42✔
551
        if err != nil {
42✔
552
                return nil, err
×
553
        }
×
554

555
        secret := &corev1.Secret{
42✔
556
                ObjectMeta: metav1.ObjectMeta{
42✔
557
                        Name:      r.configSecretKey(res).Name,
42✔
558
                        Namespace: r.configSecretKey(res).Namespace,
42✔
559
                        OwnerReferences: []metav1.OwnerReference{
42✔
560
                                {
42✔
561
                                        APIVersion:         solarv1alpha1.SchemeGroupVersion.String(),
42✔
562
                                        Kind:               res.Kind,
42✔
563
                                        Name:               res.Name,
42✔
564
                                        UID:                res.UID,
42✔
565
                                        Controller:         new(true),
42✔
566
                                        BlockOwnerDeletion: new(true),
42✔
567
                                },
42✔
568
                        },
42✔
569
                        Annotations: map[string]string{
42✔
570
                                annotationSecretName: r.configSecretKey(res).Name,
42✔
571
                        },
42✔
572
                },
42✔
573
                Type: corev1.SecretTypeOpaque,
42✔
574
                Data: map[string][]byte{
42✔
575
                        "config.json": cfgJson,
42✔
576
                },
42✔
577
        }
42✔
578

42✔
579
        if err := r.Create(ctx, secret); err != nil {
42✔
UNCOV
580
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
UNCOV
581
                return nil, errLogAndWrap(log, err, "secret creation failed")
×
UNCOV
582
        }
×
583

584
        // Set owner references
585
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
42✔
586
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
587
        }
×
588

589
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
42✔
590
                APIVersion: corev1.SchemeGroupVersion.String(),
42✔
591
                Kind:       "Secret",
42✔
592
                Namespace:  secret.Namespace,
42✔
593
                Name:       secret.Name,
42✔
594
        }
42✔
595

42✔
596
        if err := r.Status().Update(ctx, res); err != nil {
42✔
597
                return nil, errLogAndWrap(log, err, "failed to update status")
×
598
        }
×
599

600
        return secret, nil
42✔
601
}
602

603
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
245✔
604
        return client.ObjectKey{
245✔
605
                Name:      fmt.Sprintf("render-%s", res.Name),
245✔
606
                Namespace: res.Namespace,
245✔
607
        }
245✔
608
}
245✔
609

610
func (r *RenderTaskReconciler) authSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
203✔
611
        return client.ObjectKey{
203✔
612
                Name:      fmt.Sprintf("auth-%s", res.Name),
203✔
613
                Namespace: res.Namespace,
203✔
614
        }
203✔
615
}
203✔
616

617
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
203✔
618
        return client.ObjectKey{
203✔
619
                Name:      fmt.Sprintf("render-%s", res.Name),
203✔
620
                Namespace: res.Namespace,
203✔
621
        }
203✔
622
}
203✔
623

624
// isJobComplete returns true if the Job is complete
625
func isJobComplete(job *batchv1.Job) bool {
177✔
626
        return job.Status.CompletionTime != nil
177✔
627
}
177✔
628

629
func (r *RenderTaskReconciler) referenceURL(repo string, tag string) string {
48✔
630
        base := r.BaseURL
48✔
631
        if !strings.HasPrefix(base, "oci://") {
96✔
632
                base = fmt.Sprintf("oci://%s", base)
48✔
633
        }
48✔
634
        base = strings.TrimSuffix(base, "/")
48✔
635
        url := fmt.Sprintf("%s/%s:%s", base, repo, tag)
48✔
636

48✔
637
        return url
48✔
638
}
639

640
// SetupWithManager sets up the controller with the Manager.
641
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
642
        return ctrl.NewControllerManagedBy(mgr).
1✔
643
                For(&solarv1alpha1.RenderTask{}).
1✔
644
                Owns(&batchv1.Job{}).
1✔
645
                Owns(&corev1.Secret{}).
1✔
646
                Complete(r)
1✔
647
}
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