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

opendefensecloud / solution-arsenal / 23764791425

30 Mar 2026 07:57PM UTC coverage: 72.056% (-0.09%) from 72.141%
23764791425

Pull #338

github

web-flow
Merge 4bff943de into 0fe88d665
Pull Request #338: feat: reworked rendertask to be a clusterwide ressource, modified tes…

81 of 94 new or added lines in 5 files covered. (86.17%)

20 existing lines in 5 files now uncovered.

2215 of 3074 relevant lines covered (72.06%)

29.01 hits per line

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

90.89
/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
        // Namespace is the namespace where Jobs and Secrets are created.
55
        // Passed via --namespace flag from the controller-manager.
56
        Namespace string
57
}
58

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

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

443✔
72
        log.V(1).Info("RenderTask is being reconciled", "req", req)
443✔
73

443✔
74
        // Fetch the RenderTask instance
443✔
75
        res := &solarv1alpha1.RenderTask{}
443✔
76
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
493✔
77
                if apierrors.IsNotFound(err) {
100✔
78
                        // Object not found, return. Created objects are automatically garbage collected.
50✔
79
                        return ctrlResult, nil
50✔
80
                }
50✔
81

82
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
83
        }
84

85
        // Handle deletion: cleanup job and secret, then remove finalizer
86
        if !res.DeletionTimestamp.IsZero() {
441✔
87
                log.V(1).Info("RenderTask is being deleted")
48✔
88
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "RenderTask is being deleted, cleaning up secret and job")
48✔
89

48✔
90
                // Cleanup render resources, if exists
48✔
91
                if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
48✔
92
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up render job")
×
93
                }
×
94

95
                if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
48✔
96
                        return ctrlResult, errLogAndWrap(log, err, "failed to clean up render secret")
×
97
                }
×
98

99
                // Remove finalizer
100
                if slices.Contains(res.Finalizers, renderTaskFinalizer) {
96✔
101
                        log.V(1).Info("Removing finalizer from resource")
48✔
102
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
96✔
103
                                return f == renderTaskFinalizer
48✔
104
                        })
48✔
105
                        if err := r.Update(ctx, res); err != nil {
83✔
106
                                if apierrors.IsNotFound(err) {
70✔
107
                                        return ctrlResult, nil
35✔
108
                                }
35✔
109

UNCOV
110
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
×
111
                        }
112
                }
113

114
                return ctrlResult, nil
13✔
115
        }
116

117
        // Add finalizer if not present and not deleting
118
        if res.DeletionTimestamp.IsZero() {
690✔
119
                if !slices.Contains(res.Finalizers, renderTaskFinalizer) {
394✔
120
                        log.V(1).Info("Adding finalizer to resource")
49✔
121
                        res.Finalizers = append(res.Finalizers, renderTaskFinalizer)
49✔
122
                        if err := r.Update(ctx, res); err != nil {
51✔
123
                                return ctrlResult, errLogAndWrap(log, err, "failed to add finalizer")
2✔
124
                        }
2✔
125
                        // Return without requeue; the Update event will trigger reconciliation again
126
                        return ctrlResult, nil
47✔
127
                }
128
        }
129

130
        // Check if renderjob has already completed successfully
131
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
296✔
132
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
303✔
133
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
7✔
134
                return ctrlResult, nil
7✔
135
        }
7✔
136

137
        // Reconcile Config Secret
138
        configSecret := &corev1.Secret{}
289✔
139
        err := r.Get(ctx, r.configSecretKey(res), configSecret)
289✔
140
        if err != nil && apierrors.IsNotFound(err) {
507✔
141
                createdSecret, err := r.createConfigSecret(ctx, res)
218✔
142
                if err != nil {
220✔
143
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", fmt.Sprintf("Failed to create config secret: %s", err))
2✔
144
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
2✔
145
                }
2✔
146
                configSecret = createdSecret
216✔
147
        } else if err != nil {
71✔
148
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
149
        }
×
150

151
        // Resolve push secret (lives in controller namespace, referenced directly by Jobs)
152
        var pushSecret *corev1.Secret
287✔
153
        if r.PushSecretRef != nil {
574✔
154
                pushSecret = &corev1.Secret{}
287✔
155
                if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, pushSecret); err != nil {
287✔
NEW
156
                        return ctrlResult, errLogAndWrap(log, err, "failed to get push secret")
×
UNCOV
157
                }
×
158
        }
159

160
        // Reconcile Job
161
        job := &batchv1.Job{}
287✔
162
        err = r.Get(ctx, r.renderJobKey(res), job)
287✔
163
        if err != nil && apierrors.IsNotFound(err) {
332✔
164
                err := r.createRenderJob(ctx, res, configSecret, pushSecret)
45✔
165
                if err != nil {
47✔
166
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", fmt.Sprintf("Failed to create job: %s", err))
2✔
167
                        return ctrlResult, errLogAndWrap(log, err, "failed to create job")
2✔
168
                }
2✔
169
        } else if err != nil {
242✔
170
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
171
        }
×
172

173
        // Update Status
174
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
338✔
175
                if err := r.Status().Update(ctx, res); err != nil {
58✔
176
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
5✔
177
                }
5✔
178
        }
179

180
        if !isJobComplete(job) {
375✔
181
                log.V(1).Info("Job is still running, requeue after 5 seconds")
95✔
182
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
95✔
183
        }
95✔
184

185
        ttlDuration := time.Duration(ttlSeconds(res.Spec.FailedJobTTL)) * time.Second
185✔
186

185✔
187
        switch {
185✔
188
        case job.Status.Succeeded > 0:
3✔
189
                cleanupRenderResources(ctx, r, res, job)
3✔
190
                log.V(1).Info("Cleaned up after successful job")
3✔
191

3✔
192
                return ctrlResult, nil
3✔
193

194
        case job.Status.Failed > 0:
182✔
195
                if shouldCleanupSecrets(res, ttlDuration) {
360✔
196
                        cleanupSecrets(ctx, r, res)
178✔
197
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
178✔
198

178✔
199
                        return ctrlResult, nil
178✔
200
                }
178✔
201
                remaining := remainingTTL(res, ttlDuration)
4✔
202
                log.V(1).Info("Waiting for TTL to expire before cleaning up secrets", "remainingSeconds", remaining.Seconds())
4✔
203

4✔
204
                return ctrl.Result{RequeueAfter: remaining + time.Second}, nil
4✔
205
        }
206

207
        return ctrlResult, nil
×
208
}
209

210
// updateResourceStatusFromJob updates the resource status based on job status
211
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
285✔
212
        log := ctrl.LoggerFrom(ctx)
285✔
213

285✔
214
        if job == nil {
285✔
215
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
216
                        Type:               ConditionTypeJobScheduled,
×
217
                        Status:             metav1.ConditionFalse,
×
218
                        ObservedGeneration: res.Generation,
×
219
                        Reason:             "DoesNotExist",
×
220
                        Message:            "Renderer job does not exist",
×
221
                })
×
222

×
223
                return changed
×
224
        }
×
225

226
        if job.Status.Succeeded > 0 {
288✔
227
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3✔
228
                        Type:               ConditionTypeJobSucceeded,
3✔
229
                        Status:             metav1.ConditionTrue,
3✔
230
                        ObservedGeneration: res.Generation,
3✔
231
                        Reason:             "JobSucceeded",
3✔
232
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3✔
233
                })
3✔
234

3✔
235
                if res.Status.ChartURL != r.referenceURL(res.Spec.Repository, res.Spec.Tag) {
6✔
236
                        res.Status.ChartURL = r.referenceURL(res.Spec.Repository, res.Spec.Tag)
3✔
237
                        changed = true
3✔
238
                }
3✔
239

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

3✔
243
                return changed
3✔
244
        }
245

246
        if job.Status.Failed > 0 {
464✔
247
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
182✔
248
                        Type:               ConditionTypeJobFailed,
182✔
249
                        Status:             metav1.ConditionTrue,
182✔
250
                        ObservedGeneration: res.Generation,
182✔
251
                        Reason:             "JobFailed",
182✔
252
                        Message:            "Renderer job failed",
182✔
253
                })
182✔
254
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
182✔
255
                log.V(1).Info("Job failed", "name", job.Name)
182✔
256

182✔
257
                return changed
182✔
258
        }
182✔
259

260
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
100✔
261
                Type:               ConditionTypeJobScheduled,
100✔
262
                Status:             metav1.ConditionTrue,
100✔
263
                ObservedGeneration: res.Generation,
100✔
264
                Reason:             "JobScheduled",
100✔
265
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
100✔
266
        })
100✔
267
}
268

269
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask) error {
51✔
270
        job := &batchv1.Job{}
51✔
271
        if err := r.Get(ctx, r.renderJobKey(res), job); err != nil {
59✔
272
                return err
8✔
273
        }
8✔
274

275
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
43✔
276
}
277

278
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
229✔
279
        secret := &corev1.Secret{}
229✔
280
        if err := r.Get(ctx, r.configSecretKey(res), secret); err != nil {
238✔
281
                return err
9✔
282
        }
9✔
283

284
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
220✔
285
}
286

287
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret *corev1.Secret) error {
45✔
288
        log := ctrl.LoggerFrom(ctx)
45✔
289

45✔
290
        jobName := r.renderJobKey(res).Name
45✔
291
        backoffLimit := int32(3)
45✔
292
        ttlSecondsAfterFinished := int32(3600)
45✔
293
        if res.Spec.FailedJobTTL != nil {
48✔
294
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
3✔
295
        }
3✔
296

297
        volumes := []corev1.Volume{
45✔
298
                {
45✔
299
                        Name: "config",
45✔
300
                        VolumeSource: corev1.VolumeSource{
45✔
301
                                Secret: &corev1.SecretVolumeSource{
45✔
302
                                        SecretName: configSecret.Name,
45✔
303
                                        Items: []corev1.KeyToPath{
45✔
304
                                                {
45✔
305
                                                        Key:  "config.json",
45✔
306
                                                        Path: "config.json",
45✔
307
                                                },
45✔
308
                                        },
45✔
309
                                },
45✔
310
                        },
45✔
311
                },
45✔
312
        }
45✔
313
        volumeMounts := []corev1.VolumeMount{
45✔
314
                {
45✔
315
                        Name:      "config",
45✔
316
                        MountPath: "/etc/renderer/config.json",
45✔
317
                        SubPath:   "config.json",
45✔
318
                        ReadOnly:  true,
45✔
319
                },
45✔
320
        }
45✔
321
        envVars := []corev1.EnvVar{
45✔
322
                {
45✔
323
                        Name: "POD_NAMESPACE",
45✔
324
                        ValueFrom: &corev1.EnvVarSource{
45✔
325
                                FieldRef: &corev1.ObjectFieldSelector{
45✔
326
                                        FieldPath: "metadata.namespace",
45✔
327
                                },
45✔
328
                        },
45✔
329
                },
45✔
330
                {
45✔
331
                        Name: "POD_NAME",
45✔
332
                        ValueFrom: &corev1.EnvVarSource{
45✔
333
                                FieldRef: &corev1.ObjectFieldSelector{
45✔
334
                                        FieldPath: "metadata.name",
45✔
335
                                },
45✔
336
                        },
45✔
337
                },
45✔
338
        }
45✔
339

45✔
340
        if r.RendererCAConfigMap != "" {
90✔
341
                volumes = append(volumes, corev1.Volume{
45✔
342
                        Name: "ca-bundle",
45✔
343
                        VolumeSource: corev1.VolumeSource{
45✔
344
                                ConfigMap: &corev1.ConfigMapVolumeSource{
45✔
345
                                        LocalObjectReference: corev1.LocalObjectReference{
45✔
346
                                                Name: r.RendererCAConfigMap,
45✔
347
                                        },
45✔
348
                                        Items: []corev1.KeyToPath{
45✔
349
                                                {
45✔
350
                                                        Key:  "trust-bundle.pem",
45✔
351
                                                        Path: "ca-bundle.pem",
45✔
352
                                                },
45✔
353
                                        },
45✔
354
                                },
45✔
355
                        },
45✔
356
                })
45✔
357
                volumeMounts = append(volumeMounts, corev1.VolumeMount{
45✔
358
                        Name:      "ca-bundle",
45✔
359
                        MountPath: "/etc/ssl/certs",
45✔
360
                        ReadOnly:  true,
45✔
361
                })
45✔
362
                envVars = append(envVars, corev1.EnvVar{
45✔
363
                        Name:  "SSL_CERT_FILE",
45✔
364
                        Value: "/etc/ssl/certs/ca-bundle.pem",
45✔
365
                })
45✔
366
        }
45✔
367

368
        job := &batchv1.Job{
45✔
369
                ObjectMeta: metav1.ObjectMeta{
45✔
370
                        Name:      jobName,
45✔
371
                        Namespace: r.renderJobKey(res).Namespace,
45✔
372
                        OwnerReferences: []metav1.OwnerReference{
45✔
373
                                {
45✔
374
                                        APIVersion:         solarv1alpha1.SchemeGroupVersion.String(),
45✔
375
                                        Kind:               res.Kind,
45✔
376
                                        Name:               res.Name,
45✔
377
                                        UID:                res.GetUID(),
45✔
378
                                        Controller:         new(true),
45✔
379
                                        BlockOwnerDeletion: new(true),
45✔
380
                                },
45✔
381
                        },
45✔
382
                        Annotations: map[string]string{
45✔
383
                                annotationJobName: jobName,
45✔
384
                        },
45✔
385
                },
45✔
386
                Spec: batchv1.JobSpec{
45✔
387
                        BackoffLimit:            &backoffLimit,
45✔
388
                        TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
45✔
389
                        Template: corev1.PodTemplateSpec{
45✔
390
                                Spec: corev1.PodSpec{
45✔
391
                                        RestartPolicy: corev1.RestartPolicyNever,
45✔
392
                                        Containers: []corev1.Container{
45✔
393
                                                {
45✔
394
                                                        Name:    "renderer",
45✔
395
                                                        Image:   r.RendererImage,
45✔
396
                                                        Command: []string{r.RendererCommand},
45✔
397
                                                        Args: append(r.RendererArgs,
45✔
398
                                                                "/etc/renderer/config.json",
45✔
399
                                                                fmt.Sprintf("--url=%s", r.referenceURL(res.Spec.Repository, res.Spec.Tag)),
45✔
400
                                                        ),
45✔
401
                                                        Env:          envVars,
45✔
402
                                                        VolumeMounts: volumeMounts,
45✔
403
                                                },
45✔
404
                                        },
45✔
405
                                        Volumes: volumes,
45✔
406
                                },
45✔
407
                        },
45✔
408
                },
45✔
409
        }
45✔
410

45✔
411
        if pushSecret != nil {
90✔
412
                switch pushSecret.Type {
45✔
413
                case corev1.SecretTypeBasicAuth:
1✔
414
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1✔
415
                                corev1.EnvVar{
1✔
416
                                        Name: "REGISTRY_USERNAME",
1✔
417
                                        ValueFrom: &corev1.EnvVarSource{
1✔
418
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
419
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
420
                                                                Name: pushSecret.Name,
1✔
421
                                                        },
1✔
422
                                                        Key: "username",
1✔
423
                                                },
1✔
424
                                        },
1✔
425
                                },
1✔
426
                                corev1.EnvVar{
1✔
427
                                        Name: "REGISTRY_PASSWORD",
1✔
428
                                        ValueFrom: &corev1.EnvVarSource{
1✔
429
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
430
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
431
                                                                Name: pushSecret.Name,
1✔
432
                                                        },
1✔
433
                                                        Key: "password",
1✔
434
                                                },
1✔
435
                                        },
1✔
436
                                },
1✔
437
                        )
1✔
438

439
                case corev1.SecretTypeDockerConfigJson:
30✔
440
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
30✔
441
                                Name: "dockerconfig",
30✔
442
                                VolumeSource: corev1.VolumeSource{
30✔
443
                                        Secret: &corev1.SecretVolumeSource{
30✔
444
                                                SecretName: pushSecret.Name,
30✔
445
                                                Items: []corev1.KeyToPath{
30✔
446
                                                        {
30✔
447
                                                                Key:  ".dockerconfigjson",
30✔
448
                                                                Path: "dockerconfig.json",
30✔
449
                                                        },
30✔
450
                                                },
30✔
451
                                        },
30✔
452
                                },
30✔
453
                        })
30✔
454

30✔
455
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
30✔
456
                                Name:      "dockerconfig",
30✔
457
                                MountPath: "/etc/renderer/dockerconfig.json",
30✔
458
                                SubPath:   "dockerconfig.json",
30✔
459
                                ReadOnly:  true,
30✔
460
                        })
30✔
461

30✔
462
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
30✔
463
                                Name:  "DOCKER_CONFIG",
30✔
464
                                Value: "/etc/renderer/dockerconfig.json",
30✔
465
                        })
30✔
466
                default:
14✔
467
                }
468
        }
469

470
        if err := r.Create(ctx, job); err != nil {
45✔
UNCOV
471
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
×
UNCOV
472
                return errLogAndWrap(log, err, "job creation failed")
×
UNCOV
473
        }
×
474

475
        // Set owner references
476
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
45✔
477
                return errLogAndWrap(log, err, "failed to set controller reference")
×
478
        }
×
479

480
        res.Status.JobRef = &corev1.ObjectReference{
45✔
481
                APIVersion: batchv1.SchemeGroupVersion.String(),
45✔
482
                Kind:       "Job",
45✔
483
                Namespace:  job.Namespace,
45✔
484
                Name:       job.Name,
45✔
485
        }
45✔
486

45✔
487
        if err := r.Status().Update(ctx, res); err != nil {
47✔
488
                return errLogAndWrap(log, err, "failed to update status")
2✔
489
        }
2✔
490

491
        return nil
43✔
492
}
493

494
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
218✔
495
        log := ctrl.LoggerFrom(ctx)
218✔
496

218✔
497
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
218✔
498
        if err != nil {
218✔
499
                return nil, err
×
500
        }
×
501

502
        secret := &corev1.Secret{
218✔
503
                ObjectMeta: metav1.ObjectMeta{
218✔
504
                        Name:      r.configSecretKey(res).Name,
218✔
505
                        Namespace: r.configSecretKey(res).Namespace,
218✔
506
                        OwnerReferences: []metav1.OwnerReference{
218✔
507
                                {
218✔
508
                                        APIVersion:         solarv1alpha1.SchemeGroupVersion.String(),
218✔
509
                                        Kind:               res.Kind,
218✔
510
                                        Name:               res.Name,
218✔
511
                                        UID:                res.UID,
218✔
512
                                        Controller:         new(true),
218✔
513
                                        BlockOwnerDeletion: new(true),
218✔
514
                                },
218✔
515
                        },
218✔
516
                        Annotations: map[string]string{
218✔
517
                                annotationSecretName: r.configSecretKey(res).Name,
218✔
518
                        },
218✔
519
                },
218✔
520
                Type: corev1.SecretTypeOpaque,
218✔
521
                Data: map[string][]byte{
218✔
522
                        "config.json": cfgJson,
218✔
523
                },
218✔
524
        }
218✔
525

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

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

536
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
218✔
537
                APIVersion: corev1.SchemeGroupVersion.String(),
218✔
538
                Kind:       "Secret",
218✔
539
                Namespace:  secret.Namespace,
218✔
540
                Name:       secret.Name,
218✔
541
        }
218✔
542

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

547
        return secret, nil
216✔
548
}
549

550
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
1,172✔
551
        return client.ObjectKey{
1,172✔
552
                Name:      fmt.Sprintf("render-%s", res.Name),
1,172✔
553
                Namespace: r.Namespace,
1,172✔
554
        }
1,172✔
555
}
1,172✔
556

557
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
428✔
558
        return client.ObjectKey{
428✔
559
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), 63),
428✔
560
                Namespace: r.Namespace,
428✔
561
        }
428✔
562
}
428✔
563

564
// isJobComplete returns true if the Job is complete (succeeded or failed)
565
func isJobComplete(job *batchv1.Job) bool {
280✔
566
        return job.Status.CompletionTime != nil || job.Status.Failed > 0
280✔
567
}
280✔
568

569
func (r *RenderTaskReconciler) referenceURL(repo string, tag string) string {
51✔
570
        base := r.BaseURL
51✔
571
        if !strings.HasPrefix(base, "oci://") {
102✔
572
                base = fmt.Sprintf("oci://%s", base)
51✔
573
        }
51✔
574
        base = strings.TrimSuffix(base, "/")
51✔
575
        url := fmt.Sprintf("%s/%s:%s", base, repo, tag)
51✔
576

51✔
577
        return url
51✔
578
}
579

580
func ttlSeconds(ttl *int32) int32 {
185✔
581
        if ttl != nil {
365✔
582
                return *ttl
180✔
583
        }
180✔
584

585
        return 3600
5✔
586
}
587

588
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
182✔
589
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
182✔
590
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
182✔
591
}
182✔
592

593
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
4✔
594
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4✔
595
        if cond == nil {
4✔
596
                return ttl
×
597
        }
×
598
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4✔
599
        if remaining < 0 {
4✔
600
                return 0
×
601
        }
×
602

603
        return remaining
4✔
604
}
605

606
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask) {
181✔
607
        if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
181✔
608
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret", err)
×
609
        }
×
610
}
611

612
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job) {
3✔
613
        cleanupSecrets(ctx, r, res)
3✔
614
        if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
615
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job", err)
×
616
        }
×
617
}
618

619
// SetupWithManager sets up the controller with the Manager.
620
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
621
        return ctrl.NewControllerManagedBy(mgr).
1✔
622
                For(&solarv1alpha1.RenderTask{}).
1✔
623
                Owns(&batchv1.Job{}).
1✔
624
                Owns(&corev1.Secret{}).
1✔
625
                Complete(r)
1✔
626
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc