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

opendefensecloud / solution-arsenal / 23784760798

31 Mar 2026 07:01AM UTC coverage: 71.547% (-0.6%) from 72.141%
23784760798

push

github

web-flow
Merge pull request #334 from opendefensecloud/fix/155-remove-polling-render-jobs

Remove polling render jobs in RenderTask controller

6 of 12 new or added lines in 1 file covered. (50.0%)

17 existing lines in 4 files now uncovered.

2160 of 3019 relevant lines covered (71.55%)

33.1 hits per line

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

87.9
/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
        "strings"
11
        "time"
12

13
        batchv1 "k8s.io/api/batch/v1"
14
        corev1 "k8s.io/api/core/v1"
15
        apierrors "k8s.io/apimachinery/pkg/api/errors"
16
        apimeta "k8s.io/apimachinery/pkg/api/meta"
17
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18
        "k8s.io/apimachinery/pkg/runtime"
19
        "k8s.io/client-go/tools/events"
20
        ctrl "sigs.k8s.io/controller-runtime"
21
        "sigs.k8s.io/controller-runtime/pkg/client"
22
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
23

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

27
const (
28
        annotationJobName    = "solar.opendefense.cloud/job-name"
29
        annotationSecretName = "solar.opendefense.cloud/secret-name"
30

31
        // Condition types
32
        ConditionTypeJobScheduled = "JobScheduled"
33
        ConditionTypeJobSucceeded = "JobSucceeded"
34
        ConditionTypeJobFailed    = "JobFailed"
35

36
        ConditionTypeTaskCompleted = "TaskCompleted"
37
        ConditionTypeTaskFailed    = "TaskFailed"
38
)
39

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

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

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

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

452✔
73
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
454✔
74
                return ctrlResult, nil
2✔
75
        }
2✔
76

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

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

88
        // RenderTask instance marked for deletion, stop reconciling
89
        if !res.DeletionTimestamp.IsZero() {
438✔
UNCOV
90
                log.V(1).Info("RenderTask is being deleted")
×
UNCOV
91
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "Deleting", "Delete", "RenderTask is being deleted, cleaning up secret and job")
×
UNCOV
92

×
UNCOV
93
                return ctrlResult, nil
×
UNCOV
94
        }
×
95

96
        // Check if renderjob has already completed successfully
97
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
438✔
98
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
446✔
99
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
8✔
100
                return ctrlResult, nil
8✔
101
        }
8✔
102

103
        // Reconcile Config Secret
104
        configSecret := &corev1.Secret{}
430✔
105
        err := r.Get(ctx, r.configSecretKey(res), configSecret)
430✔
106
        if err != nil && apierrors.IsNotFound(err) {
780✔
107
                createdSecret, err := r.createConfigSecret(ctx, res)
350✔
108
                if err != nil {
351✔
109
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", fmt.Sprintf("Failed to create config secret: %s", err))
1✔
110
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
1✔
111
                }
1✔
112
                configSecret = createdSecret
349✔
113
        } else if err != nil {
80✔
114
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
115
        }
×
116

117
        // Reconcile Auth Secret
118
        authSecret := &corev1.Secret{}
429✔
119
        err = r.Get(ctx, r.authSecretKey(res), authSecret)
429✔
120
        if err != nil && apierrors.IsNotFound(err) && r.PushSecretRef != nil {
778✔
121
                createdSecret, err := r.copyAuthSecret(ctx, res)
349✔
122
                if err != nil {
351✔
123
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateAuthSecret", fmt.Sprintf("Failed to create auth secret: %s", err))
2✔
124
                        return ctrlResult, errLogAndWrap(log, err, "failed to copy auth secret to namespace")
2✔
125
                }
2✔
126
                authSecret = createdSecret
347✔
127
        } else if client.IgnoreNotFound(err) != nil {
80✔
128
                return ctrlResult, errLogAndWrap(log, err, "could not get auth secret")
×
129
        }
×
130

131
        // Reconcile Job
132
        job := &batchv1.Job{}
427✔
133
        err = r.Get(ctx, r.renderJobKey(res), job)
427✔
134
        if err != nil && apierrors.IsNotFound(err) {
475✔
135
                err := r.createRenderJob(ctx, res, configSecret, authSecret)
48✔
136
                if err != nil {
48✔
137
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", fmt.Sprintf("Failed to create job: %s", err))
×
138
                        return ctrlResult, errLogAndWrap(log, err, "failed to create job")
×
139
                }
×
140
        } else if err != nil {
379✔
141
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
142
        }
×
143

144
        // Update Status
145
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
493✔
146
                if err := r.Status().Update(ctx, res); err != nil {
79✔
147
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
13✔
148
                }
13✔
149
        }
150

151
        ttlDuration := time.Duration(ttlSeconds(res.Spec.FailedJobTTL)) * time.Second
414✔
152

414✔
153
        switch {
414✔
154
        case job.Status.Succeeded > 0:
3✔
155
                cleanupRenderResources(ctx, r, res, job)
3✔
156
                log.V(1).Info("Cleaned up after successful job")
3✔
157

3✔
158
                return ctrlResult, nil
3✔
159

160
        case job.Status.Failed > 0:
306✔
161
                if shouldCleanupSecrets(res, ttlDuration) {
608✔
162
                        cleanupSecrets(ctx, r, res)
302✔
163
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
302✔
164

302✔
165
                        return ctrlResult, nil
302✔
166
                }
302✔
167
                remaining := remainingTTL(res, ttlDuration)
4✔
168
                log.V(1).Info("Waiting for TTL to expire before cleaning up secrets", "remainingSeconds", remaining.Seconds())
4✔
169

4✔
170
                return ctrl.Result{RequeueAfter: remaining + time.Second}, nil
4✔
171
        }
172

173
        return ctrlResult, nil
105✔
174
}
175

176
// updateResourceStatusFromJob updates the resource status based on job status
177
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
427✔
178
        log := ctrl.LoggerFrom(ctx)
427✔
179

427✔
180
        if job == nil {
427✔
181
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
182
                        Type:               ConditionTypeJobScheduled,
×
183
                        Status:             metav1.ConditionFalse,
×
184
                        ObservedGeneration: res.Generation,
×
185
                        Reason:             "DoesNotExist",
×
186
                        Message:            "Renderer job does not exist",
×
187
                })
×
188

×
189
                return changed
×
190
        }
×
191

192
        if job.Status.Succeeded > 0 {
430✔
193
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3✔
194
                        Type:               ConditionTypeJobSucceeded,
3✔
195
                        Status:             metav1.ConditionTrue,
3✔
196
                        ObservedGeneration: res.Generation,
3✔
197
                        Reason:             "JobSucceeded",
3✔
198
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3✔
199
                })
3✔
200

3✔
201
                if res.Status.ChartURL != r.referenceURL(res.Spec.Repository, res.Spec.Tag) {
6✔
202
                        res.Status.ChartURL = r.referenceURL(res.Spec.Repository, res.Spec.Tag)
3✔
203
                        changed = true
3✔
204
                }
3✔
205

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

3✔
209
                return changed
3✔
210
        }
211

212
        if job.Status.Failed > 0 {
730✔
213
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
306✔
214
                        Type:               ConditionTypeJobFailed,
306✔
215
                        Status:             metav1.ConditionTrue,
306✔
216
                        ObservedGeneration: res.Generation,
306✔
217
                        Reason:             "JobFailed",
306✔
218
                        Message:            "Renderer job failed",
306✔
219
                })
306✔
220
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
306✔
221
                log.V(1).Info("Job failed", "name", job.Name)
306✔
222

306✔
223
                return changed
306✔
224
        }
306✔
225

226
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
118✔
227
                Type:               ConditionTypeJobScheduled,
118✔
228
                Status:             metav1.ConditionTrue,
118✔
229
                ObservedGeneration: res.Generation,
118✔
230
                Reason:             "JobScheduled",
118✔
231
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
118✔
232
        })
118✔
233
}
234

235
func (r *RenderTaskReconciler) deleteAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
305✔
236
        secret := &corev1.Secret{}
305✔
237
        if err := r.Get(ctx, r.authSecretKey(res), secret); err != nil {
308✔
238
                return err
3✔
239
        }
3✔
240

241
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
302✔
242
}
243

244
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask) error {
3✔
245
        job := &batchv1.Job{}
3✔
246
        if err := r.Get(ctx, r.renderJobKey(res), job); err != nil {
3✔
UNCOV
247
                return err
×
UNCOV
248
        }
×
249

250
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
3✔
251
}
252

253
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
305✔
254
        secret := &corev1.Secret{}
305✔
255
        if err := r.Get(ctx, r.configSecretKey(res), secret); err != nil {
305✔
UNCOV
256
                return err
×
UNCOV
257
        }
×
258

259
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
305✔
260
}
261

262
func (r *RenderTaskReconciler) copyAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
349✔
263
        log := ctrl.LoggerFrom(ctx)
349✔
264

349✔
265
        controllerSecret := &corev1.Secret{}
349✔
266
        if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, controllerSecret); err != nil {
349✔
267
                return nil, err
×
268
        }
×
269

270
        authSecret := &corev1.Secret{
349✔
271
                ObjectMeta: metav1.ObjectMeta{
349✔
272
                        Name:      r.authSecretKey(res).Name,
349✔
273
                        Namespace: r.authSecretKey(res).Namespace,
349✔
274
                },
349✔
275
                Type:       controllerSecret.Type,
349✔
276
                Data:       controllerSecret.Data,
349✔
277
                StringData: controllerSecret.StringData,
349✔
278
        }
349✔
279

349✔
280
        // Set owner references
349✔
281
        if err := controllerutil.SetControllerReference(res, authSecret, r.Scheme); err != nil {
349✔
282
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
283
        }
×
284

285
        if err := r.Create(ctx, authSecret); err != nil {
352✔
286
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
3✔
287
                return nil, errLogAndWrap(log, err, "secret creation failed")
3✔
288
        }
3✔
289

290
        return authSecret, nil
346✔
291
}
292

293
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, authSecret *corev1.Secret) error {
48✔
294
        log := ctrl.LoggerFrom(ctx)
48✔
295

48✔
296
        jobName := r.renderJobKey(res).Name
48✔
297
        backoffLimit := int32(3)
48✔
298
        ttlSecondsAfterFinished := int32(3600)
48✔
299
        if res.Spec.FailedJobTTL != nil {
51✔
300
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
3✔
301
        }
3✔
302

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

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

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

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

435
                case corev1.SecretTypeDockerConfigJson:
17✔
436
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
17✔
437
                                Name: "dockerconfig",
17✔
438
                                VolumeSource: corev1.VolumeSource{
17✔
439
                                        Secret: &corev1.SecretVolumeSource{
17✔
440
                                                SecretName: authSecret.Name,
17✔
441
                                                Items: []corev1.KeyToPath{
17✔
442
                                                        {
17✔
443
                                                                Key:  ".dockerconfigjson",
17✔
444
                                                                Path: "dockerconfig.json",
17✔
445
                                                        },
17✔
446
                                                },
17✔
447
                                        },
17✔
448
                                },
17✔
449
                        })
17✔
450

17✔
451
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
17✔
452
                                Name:      "dockerconfig",
17✔
453
                                MountPath: "/etc/renderer/dockerconfig.json",
17✔
454
                                SubPath:   "dockerconfig.json",
17✔
455
                                ReadOnly:  true,
17✔
456
                        })
17✔
457

17✔
458
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
17✔
459
                                Name:  "DOCKER_CONFIG",
17✔
460
                                Value: "/etc/renderer/dockerconfig.json",
17✔
461
                        })
17✔
462
                default:
30✔
463
                }
464
        }
465

466
        // Set owner references
467
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
48✔
468
                return errLogAndWrap(log, err, "failed to set controller reference")
×
469
        }
×
470

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

476
        res.Status.JobRef = &corev1.ObjectReference{
48✔
477
                APIVersion: batchv1.SchemeGroupVersion.String(),
48✔
478
                Kind:       "Job",
48✔
479
                Namespace:  job.Namespace,
48✔
480
                Name:       job.Name,
48✔
481
        }
48✔
482

48✔
483
        if err := r.Status().Update(ctx, res); err != nil {
48✔
484
                return errLogAndWrap(log, err, "failed to update status")
×
485
        }
×
486

487
        return nil
48✔
488
}
489

490
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
350✔
491
        log := ctrl.LoggerFrom(ctx)
350✔
492

350✔
493
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
350✔
494
        if err != nil {
350✔
495
                return nil, err
×
496
        }
×
497

498
        secret := &corev1.Secret{
350✔
499
                ObjectMeta: metav1.ObjectMeta{
350✔
500
                        Name:      r.configSecretKey(res).Name,
350✔
501
                        Namespace: r.configSecretKey(res).Namespace,
350✔
502
                        Annotations: map[string]string{
350✔
503
                                annotationSecretName: r.configSecretKey(res).Name,
350✔
504
                        },
350✔
505
                },
350✔
506
                Type: corev1.SecretTypeOpaque,
350✔
507
                Data: map[string][]byte{
350✔
508
                        "config.json": cfgJson,
350✔
509
                },
350✔
510
        }
350✔
511

350✔
512
        // Set owner references
350✔
513
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
350✔
514
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
515
        }
×
516

517
        if err := r.Create(ctx, secret); err != nil {
350✔
NEW
518
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
NEW
519
                return nil, errLogAndWrap(log, err, "secret creation failed")
×
NEW
520
        }
×
521

522
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
350✔
523
                APIVersion: corev1.SchemeGroupVersion.String(),
350✔
524
                Kind:       "Secret",
350✔
525
                Namespace:  secret.Namespace,
350✔
526
                Name:       secret.Name,
350✔
527
        }
350✔
528

350✔
529
        if err := r.Status().Update(ctx, res); err != nil {
351✔
530
                return nil, errLogAndWrap(log, err, "failed to update status")
1✔
531
        }
1✔
532

533
        return secret, nil
349✔
534
}
535

536
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
1,785✔
537
        return client.ObjectKey{
1,785✔
538
                Name:      fmt.Sprintf("render-%s", res.Name),
1,785✔
539
                Namespace: res.Namespace,
1,785✔
540
        }
1,785✔
541
}
1,785✔
542

543
func (r *RenderTaskReconciler) authSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
1,432✔
544
        return client.ObjectKey{
1,432✔
545
                Name:      fmt.Sprintf("auth-%s", res.Name),
1,432✔
546
                Namespace: res.Namespace,
1,432✔
547
        }
1,432✔
548
}
1,432✔
549

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

557
func (r *RenderTaskReconciler) referenceURL(repo string, tag string) string {
54✔
558
        base := r.BaseURL
54✔
559
        if !strings.HasPrefix(base, "oci://") {
108✔
560
                base = fmt.Sprintf("oci://%s", base)
54✔
561
        }
54✔
562
        base = strings.TrimSuffix(base, "/")
54✔
563
        url := fmt.Sprintf("%s/%s:%s", base, repo, tag)
54✔
564

54✔
565
        return url
54✔
566
}
567

568
func ttlSeconds(ttl *int32) int32 {
414✔
569
        if ttl != nil {
724✔
570
                return *ttl
310✔
571
        }
310✔
572

573
        return 3600
104✔
574
}
575

576
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
306✔
577
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
306✔
578
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
306✔
579
}
306✔
580

581
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
4✔
582
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4✔
583
        if cond == nil {
4✔
584
                return ttl
×
585
        }
×
586
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4✔
587
        if remaining < 0 {
4✔
588
                return 0
×
589
        }
×
590

591
        return remaining
4✔
592
}
593

594
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask) {
305✔
595
        if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
305✔
596
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret", err)
×
597
        }
×
598
        if err := r.deleteAuthSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
305✔
599
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete auth secret", err)
×
600
        }
×
601
}
602

603
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job) {
3✔
604
        cleanupSecrets(ctx, r, res)
3✔
605
        if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
606
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job", err)
×
607
        }
×
608
}
609

610
// SetupWithManager sets up the controller with the Manager.
611
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
612
        return ctrl.NewControllerManagedBy(mgr).
1✔
613
                For(&solarv1alpha1.RenderTask{}).
1✔
614
                Owns(&batchv1.Job{}).
1✔
615
                Owns(&corev1.Secret{}).
1✔
616
                Complete(r)
1✔
617
}
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