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

opendefensecloud / solution-arsenal / 23530208184

25 Mar 2026 07:40AM UTC coverage: 71.809% (+0.4%) from 71.419%
23530208184

push

github

web-flow
feat: add configurable TTL for failed render job cleanup (#326)

* feat: add failedJobTTL field to Release for failed job cleanup

Adds a new optional field `failedJobTTL` to ReleaseSpec that allows users
to configure how long failed render jobs are retained before cleanup.
- Field propagates from Release to RenderTask to the underlying Job's
  TTLSecondsAfterFinished
- Defaults to 3600 seconds (1 hour) if not set
- Secrets (ConfigSecret, AuthSecret) are cleaned up when RenderTask is
  deleted via the existing finalizer
- Successful jobs continue to be cleaned up immediately by the controller

* docs: More explicit documentation for new field

3 of 5 new or added lines in 2 files covered. (60.0%)

9 existing lines in 3 files now uncovered.

2183 of 3040 relevant lines covered (71.81%)

13.64 hits per line

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

88.21
/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) {
192✔
71
        log := ctrl.LoggerFrom(ctx)
192✔
72
        ctrlResult := ctrl.Result{}
192✔
73

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

192✔
76
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
214✔
77
                return ctrlResult, nil
22✔
78
        }
22✔
79

80
        // Fetch the RenderTask instance
81
        res := &solarv1alpha1.RenderTask{}
170✔
82
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
182✔
83
                if apierrors.IsNotFound(err) {
24✔
84
                        // Object not found, return. Created objects are automatically garbage collected.
12✔
85
                        return ctrlResult, nil
12✔
86
                }
12✔
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() {
170✔
93
                log.V(1).Info("RenderTask is being deleted")
12✔
94
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "RenderTask is being deleted, cleaning up secret and job")
12✔
95

12✔
96
                // Cleanup render resources, if exists
12✔
97
                if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
12✔
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) {
12✔
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) {
12✔
106
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up auth secret")
×
107
                }
×
108

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

120
                return ctrlResult, nil
11✔
121
        }
122

123
        // Add finalizer if not present and not deleting
124
        if res.DeletionTimestamp.IsZero() {
292✔
125
                if !slices.Contains(res.Finalizers, renderTaskFinalizer) {
189✔
126
                        log.V(1).Info("Adding finalizer to resource")
43✔
127
                        res.Finalizers = append(res.Finalizers, renderTaskFinalizer)
43✔
128
                        if err := r.Update(ctx, res); err != nil {
43✔
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
43✔
133
                }
134
        }
135

136
        // Check if renderjob has already completed successfully
137
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
103✔
138
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
107✔
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)
99✔
145
        if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue {
102✔
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{}
96✔
152
        err := r.Get(ctx, r.configSecretKey(res), configSecret)
96✔
153
        if err != nil && apierrors.IsNotFound(err) {
138✔
154
                createdSecret, err := r.createConfigSecret(ctx, res)
42✔
155
                if err != nil {
42✔
156
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", fmt.Sprintf("Failed to create config secret: %s", err))
×
157
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
×
158
                }
×
159
                configSecret = createdSecret
42✔
160
        } else if err != nil {
54✔
161
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
162
        }
×
163

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

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

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

198
        // Check if we need to clean up
199
        if isJobComplete(job) && job.Status.Succeeded > 0 {
89✔
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) {
166✔
219
                log.V(1).Info("Job is still running, requeue after 5 seconds")
83✔
220
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
83✔
221
        }
83✔
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) {
95✔
228
        log := ctrl.LoggerFrom(ctx)
95✔
229

95✔
230
        if job == nil {
95✔
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 {
98✔
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 {
93✔
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{
91✔
277
                Type:               ConditionTypeJobScheduled,
91✔
278
                Status:             metav1.ConditionTrue,
91✔
279
                ObservedGeneration: res.Generation,
91✔
280
                Reason:             "JobScheduled",
91✔
281
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
91✔
282
        })
91✔
283
}
284

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

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

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

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

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

309
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
13✔
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✔
317
                return nil, err
×
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 {
44✔
331
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
2✔
332
                return nil, errLogAndWrap(log, err, "secret creation failed")
2✔
333
        }
2✔
334

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

340
        return authSecret, nil
40✔
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)
42✔
349
        if res.Spec.FailedJobTTL != nil {
42✔
NEW
350
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
×
NEW
351
        }
×
352

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

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

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

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

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

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

14✔
518
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
14✔
519
                                Name:  "DOCKER_CONFIG",
14✔
520
                                Value: "/etc/renderer/dockerconfig.json",
14✔
521
                        })
14✔
522
                default:
25✔
523
                }
524
        }
525

526
        if err := r.Create(ctx, job); err != nil {
44✔
527
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
2✔
528
                return errLogAndWrap(log, err, "job creation failed")
2✔
529
        }
2✔
530

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

536
        res.Status.JobRef = &corev1.ObjectReference{
40✔
537
                APIVersion: batchv1.SchemeGroupVersion.String(),
40✔
538
                Kind:       "Job",
40✔
539
                Namespace:  job.Namespace,
40✔
540
                Name:       job.Name,
40✔
541
        }
40✔
542

40✔
543
        if err := r.Status().Update(ctx, res); err != nil {
41✔
544
                return errLogAndWrap(log, err, "failed to update status")
1✔
545
        }
1✔
546

547
        return nil
39✔
548
}
549

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

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

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

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

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

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

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

603
        return secret, nil
42✔
604
}
605

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

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

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

627
// isJobComplete returns true if the Job is complete
628
func isJobComplete(job *batchv1.Job) bool {
169✔
629
        return job.Status.CompletionTime != nil
169✔
630
}
169✔
631

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

48✔
640
        return url
48✔
641
}
642

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