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

opendefensecloud / solution-arsenal / 23740481999

30 Mar 2026 10:35AM UTC coverage: 71.81% (-0.3%) from 72.141%
23740481999

Pull #341

github

web-flow
Merge 068bf0d50 into 0fe88d665
Pull Request #341: fix: ensure getHandler in discovery returns handler on subsequent calls

10 of 10 new or added lines in 1 file covered. (100.0%)

12 existing lines in 3 files now uncovered.

2206 of 3072 relevant lines covered (71.81%)

17.64 hits per line

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

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

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

274✔
76
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
307✔
77
                return ctrlResult, nil
33✔
78
        }
33✔
79

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

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

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

120
                return ctrlResult, nil
10✔
121
        }
122

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

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

143
        // Reconcile Config Secret
144
        configSecret := &corev1.Secret{}
167✔
145
        err := r.Get(ctx, r.configSecretKey(res), configSecret)
167✔
146
        if err != nil && apierrors.IsNotFound(err) {
269✔
147
                createdSecret, err := r.createConfigSecret(ctx, res)
102✔
148
                if err != nil {
102✔
149
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", fmt.Sprintf("Failed to create config secret: %s", err))
×
150
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
×
151
                }
×
152
                configSecret = createdSecret
102✔
153
        } else if err != nil {
65✔
154
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
155
        }
×
156

157
        // Reconcile Auth Secret
158
        authSecret := &corev1.Secret{}
167✔
159
        err = r.Get(ctx, r.authSecretKey(res), authSecret)
167✔
160
        if err != nil && apierrors.IsNotFound(err) && r.PushSecretRef != nil {
269✔
161
                createdSecret, err := r.copyAuthSecret(ctx, res)
102✔
162
                if err != nil {
102✔
163
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateAuthSecret", fmt.Sprintf("Failed to create auth secret: %s", err))
×
164
                        return ctrlResult, errLogAndWrap(log, err, "failed to copy auth secret to namespace")
×
165
                }
×
166
                authSecret = createdSecret
102✔
167
        } else if client.IgnoreNotFound(err) != nil {
65✔
168
                return ctrlResult, errLogAndWrap(log, err, "could not get auth secret")
×
169
        }
×
170

171
        // Reconcile Job
172
        job := &batchv1.Job{}
167✔
173
        err = r.Get(ctx, r.renderJobKey(res), job)
167✔
174
        if err != nil && apierrors.IsNotFound(err) {
214✔
175
                err := r.createRenderJob(ctx, res, configSecret, authSecret)
47✔
176
                if err != nil {
47✔
177
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", fmt.Sprintf("Failed to create job: %s", err))
×
178
                        return ctrlResult, errLogAndWrap(log, err, "failed to create job")
×
179
                }
×
180
        } else if err != nil {
120✔
181
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
182
        }
×
183

184
        // Update Status
185
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
218✔
186
                if err := r.Status().Update(ctx, res); err != nil {
59✔
187
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
5✔
188
                }
5✔
189
        }
190

191
        if !isJobComplete(job) {
255✔
192
                log.V(1).Info("Job is still running, requeue after 5 seconds")
96✔
193
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
96✔
194
        }
96✔
195

196
        ttlDuration := time.Duration(ttlSeconds(res.Spec.FailedJobTTL)) * time.Second
63✔
197

63✔
198
        switch {
63✔
199
        case job.Status.Succeeded > 0:
3✔
200
                cleanupRenderResources(ctx, r, res, job)
3✔
201
                log.V(1).Info("Cleaned up after successful job")
3✔
202

3✔
203
                return ctrlResult, nil
3✔
204

205
        case job.Status.Failed > 0:
60✔
206
                if shouldCleanupSecrets(res, ttlDuration) {
116✔
207
                        cleanupSecrets(ctx, r, res)
56✔
208
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
56✔
209

56✔
210
                        return ctrlResult, nil
56✔
211
                }
56✔
212
                remaining := remainingTTL(res, ttlDuration)
4✔
213
                log.V(1).Info("Waiting for TTL to expire before cleaning up secrets", "remainingSeconds", remaining.Seconds())
4✔
214

4✔
215
                return ctrl.Result{RequeueAfter: remaining + time.Second}, nil
4✔
216
        }
217

218
        return ctrlResult, nil
×
219
}
220

221
// updateResourceStatusFromJob updates the resource status based on job status
222
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
164✔
223
        log := ctrl.LoggerFrom(ctx)
164✔
224

164✔
225
        if job == nil {
164✔
226
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
227
                        Type:               ConditionTypeJobScheduled,
×
228
                        Status:             metav1.ConditionFalse,
×
229
                        ObservedGeneration: res.Generation,
×
230
                        Reason:             "DoesNotExist",
×
231
                        Message:            "Renderer job does not exist",
×
232
                })
×
233

×
234
                return changed
×
235
        }
×
236

237
        if job.Status.Succeeded > 0 {
167✔
238
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3✔
239
                        Type:               ConditionTypeJobSucceeded,
3✔
240
                        Status:             metav1.ConditionTrue,
3✔
241
                        ObservedGeneration: res.Generation,
3✔
242
                        Reason:             "JobSucceeded",
3✔
243
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3✔
244
                })
3✔
245

3✔
246
                if res.Status.ChartURL != r.referenceURL(res.Spec.Repository, res.Spec.Tag) {
6✔
247
                        res.Status.ChartURL = r.referenceURL(res.Spec.Repository, res.Spec.Tag)
3✔
248
                        changed = true
3✔
249
                }
3✔
250

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

3✔
254
                return changed
3✔
255
        }
256

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

60✔
268
                return changed
60✔
269
        }
60✔
270

271
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
101✔
272
                Type:               ConditionTypeJobScheduled,
101✔
273
                Status:             metav1.ConditionTrue,
101✔
274
                ObservedGeneration: res.Generation,
101✔
275
                Reason:             "JobScheduled",
101✔
276
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
101✔
277
        })
101✔
278
}
279

280
func (r *RenderTaskReconciler) deleteAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
69✔
281
        secret := &corev1.Secret{}
69✔
282
        if err := r.Get(ctx, r.authSecretKey(res), secret); err != nil {
70✔
283
                return err
1✔
284
        }
1✔
285

286
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
68✔
287
}
288

289
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask) error {
13✔
290
        job := &batchv1.Job{}
13✔
291
        if err := r.Get(ctx, r.renderJobKey(res), job); err != nil {
13✔
UNCOV
292
                return err
×
UNCOV
293
        }
×
294

295
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
13✔
296
}
297

298
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
69✔
299
        secret := &corev1.Secret{}
69✔
300
        if err := r.Get(ctx, r.configSecretKey(res), secret); err != nil {
69✔
UNCOV
301
                return err
×
UNCOV
302
        }
×
303

304
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
69✔
305
}
306

307
func (r *RenderTaskReconciler) copyAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
102✔
308
        log := ctrl.LoggerFrom(ctx)
102✔
309

102✔
310
        controllerSecret := &corev1.Secret{}
102✔
311
        if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, controllerSecret); err != nil {
102✔
312
                return nil, err
×
313
        }
×
314

315
        authSecret := &corev1.Secret{
102✔
316
                ObjectMeta: metav1.ObjectMeta{
102✔
317
                        Name:      r.authSecretKey(res).Name,
102✔
318
                        Namespace: r.authSecretKey(res).Namespace,
102✔
319
                },
102✔
320
                Type:       controllerSecret.Type,
102✔
321
                Data:       controllerSecret.Data,
102✔
322
                StringData: controllerSecret.StringData,
102✔
323
        }
102✔
324

102✔
325
        if err := r.Create(ctx, authSecret); err != nil {
107✔
326
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
5✔
327
                return nil, errLogAndWrap(log, err, "secret creation failed")
5✔
328
        }
5✔
329

330
        // Set owner references
331
        if err := controllerutil.SetControllerReference(res, authSecret, r.Scheme); err != nil {
97✔
332
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
333
        }
×
334

335
        return authSecret, nil
97✔
336
}
337

338
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, authSecret *corev1.Secret) error {
47✔
339
        log := ctrl.LoggerFrom(ctx)
47✔
340

47✔
341
        jobName := r.renderJobKey(res).Name
47✔
342
        backoffLimit := int32(3)
47✔
343
        ttlSecondsAfterFinished := int32(3600)
47✔
344
        if res.Spec.FailedJobTTL != nil {
50✔
345
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
3✔
346
        }
3✔
347

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

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

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

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

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

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

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

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

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

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

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

542
        return nil
43✔
543
}
544

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

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

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

102✔
577
        if err := r.Create(ctx, secret); err != nil {
105✔
578
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
3✔
579
                return nil, errLogAndWrap(log, err, "secret creation failed")
3✔
580
        }
3✔
581

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

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

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

598
        return secret, nil
99✔
599
}
600

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

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

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

622
// isJobComplete returns true if the Job is complete (succeeded or failed)
623
func isJobComplete(job *batchv1.Job) bool {
159✔
624
        return job.Status.CompletionTime != nil || job.Status.Failed > 0
159✔
625
}
159✔
626

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

50✔
635
        return url
50✔
636
}
637

638
func ttlSeconds(ttl *int32) int32 {
63✔
639
        if ttl != nil {
121✔
640
                return *ttl
58✔
641
        }
58✔
642

643
        return 3600
5✔
644
}
645

646
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
60✔
647
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
60✔
648
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
60✔
649
}
60✔
650

651
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
4✔
652
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4✔
653
        if cond == nil {
4✔
654
                return ttl
×
655
        }
×
656
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4✔
657
        if remaining < 0 {
4✔
658
                return 0
×
659
        }
×
660

661
        return remaining
4✔
662
}
663

664
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask) {
59✔
665
        if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
59✔
666
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret", err)
×
667
        }
×
668
        if err := r.deleteAuthSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
59✔
669
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete auth secret", err)
×
670
        }
×
671
}
672

673
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job) {
3✔
674
        cleanupSecrets(ctx, r, res)
3✔
675
        if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
676
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job", err)
×
677
        }
×
678
}
679

680
// SetupWithManager sets up the controller with the Manager.
681
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
682
        return ctrl.NewControllerManagedBy(mgr).
1✔
683
                For(&solarv1alpha1.RenderTask{}).
1✔
684
                Owns(&batchv1.Job{}).
1✔
685
                Owns(&corev1.Secret{}).
1✔
686
                Complete(r)
1✔
687
}
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