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

opendefensecloud / solution-arsenal / 23434311281

23 Mar 2026 11:08AM UTC coverage: 71.723% (-0.3%) from 71.988%
23434311281

push

github

web-flow
fix(deps): update k8s.io/utils digest to 28399d8 (#311)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

2161 of 3013 relevant lines covered (71.72%)

19.81 hits per line

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

88.73
/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
}
55

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

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

473✔
69
        log.V(1).Info("RenderTask is being reconciled", "req", req)
473✔
70

473✔
71
        // Fetch the RenderTask instance
473✔
72
        res := &solarv1alpha1.RenderTask{}
473✔
73
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
485✔
74
                if apierrors.IsNotFound(err) {
24✔
75
                        // Object not found, return. Created objects are automatically garbage collected.
12✔
76
                        return ctrlResult, nil
12✔
77
                }
12✔
78

79
                return ctrlResult, errLogAndWrap(log, err, "failed to get object")
×
80
        }
81

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

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

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

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

100
                // Remove finalizer
101
                if slices.Contains(res.Finalizers, renderTaskFinalizer) {
24✔
102
                        log.V(1).Info("Removing finalizer from resource")
12✔
103
                        res.Finalizers = slices.DeleteFunc(res.Finalizers, func(f string) bool {
24✔
104
                                return f == renderTaskFinalizer
12✔
105
                        })
12✔
106
                        if err := r.Update(ctx, res); err != nil {
13✔
107
                                return ctrlResult, errLogAndWrap(log, err, "failed to remove finalizer")
1✔
108
                        }
1✔
109
                }
110

111
                return ctrlResult, nil
11✔
112
        }
113

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

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

134
        // Check if renderjob has already failed
135
        fc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
398✔
136
        if fc != nil && fc.ObservedGeneration >= res.Generation && fc.Status == metav1.ConditionTrue {
401✔
137
                log.V(1).Info("RenderTask has already failed, no further action needed")
3✔
138
                return ctrlResult, nil
3✔
139
        }
3✔
140

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

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

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

182
        // Update Status
183
        if changed := r.updateResourceStatusFromJob(ctx, res, job); changed {
61✔
184
                if err := r.Status().Update(ctx, res); err != nil {
17✔
185
                        return ctrlResult, errLogAndWrap(log, err, "failed to update status")
×
186
                }
×
187
        }
188

189
        // Check if we need to clean up
190
        if isJobComplete(job) && job.Status.Succeeded > 0 {
47✔
191
                if err := r.deleteRenderJob(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
192
                        r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job", err)
×
193
                        return ctrlResult, nil
×
194
                }
×
195
                if err := r.deleteConfigSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
196
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret", err)
×
197
                        return ctrlResult, nil
×
198
                }
×
199
                if err := r.deleteAuthSecret(ctx, res); err != nil && !apierrors.IsNotFound(err) {
3✔
200
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete auth secret", err)
×
201
                        return ctrlResult, nil
×
202
                }
×
203
                log.V(1).Info("Cleaned up after successful job")
3✔
204

3✔
205
                return ctrlResult, nil
3✔
206
        }
207

208
        // Check if job is still running
209
        if !isJobComplete(job) {
82✔
210
                log.V(1).Info("Job is still running, requeue after 5 seconds")
41✔
211
                return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
41✔
212
        }
41✔
213

214
        return ctrlResult, nil
×
215
}
216

217
// updateResourceStatusFromJob updates the resource status based on job status
218
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
44✔
219
        log := ctrl.LoggerFrom(ctx)
44✔
220

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

×
230
                return changed
×
231
        }
×
232

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

3✔
242
                if res.Status.ChartURL != r.referenceURL(res.Spec.Repository, res.Spec.Tag) {
6✔
243
                        res.Status.ChartURL = r.referenceURL(res.Spec.Repository, res.Spec.Tag)
3✔
244
                        changed = true
3✔
245
                }
3✔
246

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

3✔
250
                return changed
3✔
251
        }
252

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

1✔
264
                return changed
1✔
265
        }
1✔
266

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

276
func (r *RenderTaskReconciler) deleteAuthSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
15✔
277
        secret := &corev1.Secret{}
15✔
278
        if err := r.Get(ctx, r.authSecretKey(res), secret); err != nil {
26✔
279
                return err
11✔
280
        }
11✔
281

282
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
4✔
283
}
284

285
func (r *RenderTaskReconciler) deleteRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask) error {
15✔
286
        job := &batchv1.Job{}
15✔
287
        if err := r.Get(ctx, r.renderJobKey(res), job); err != nil {
26✔
288
                return err
11✔
289
        }
11✔
290

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

294
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) error {
15✔
295
        secret := &corev1.Secret{}
15✔
296
        if err := r.Get(ctx, r.configSecretKey(res), secret); err != nil {
16✔
297
                return err
1✔
298
        }
1✔
299

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

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

312✔
306
        controllerSecret := &corev1.Secret{}
312✔
307
        if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, controllerSecret); err != nil {
586✔
308
                return nil, err
274✔
309
        }
274✔
310

311
        authSecret := &corev1.Secret{
38✔
312
                ObjectMeta: metav1.ObjectMeta{
38✔
313
                        Name:      r.authSecretKey(res).Name,
38✔
314
                        Namespace: r.authSecretKey(res).Namespace,
38✔
315
                },
38✔
316
                Type:       controllerSecret.Type,
38✔
317
                Data:       controllerSecret.Data,
38✔
318
                StringData: controllerSecret.StringData,
38✔
319
        }
38✔
320

38✔
321
        if err := r.Create(ctx, authSecret); err != nil {
63✔
322
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
25✔
323
                return nil, errLogAndWrap(log, err, "secret creation failed")
25✔
324
        }
25✔
325

326
        // Set owner references
327
        if err := controllerutil.SetControllerReference(res, authSecret, r.Scheme); err != nil {
13✔
328
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
329
        }
×
330

331
        return authSecret, nil
13✔
332
}
333

334
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, authSecret *corev1.Secret) error {
13✔
335
        log := ctrl.LoggerFrom(ctx)
13✔
336

13✔
337
        jobName := r.renderJobKey(res).Name
13✔
338
        backoffLimit := int32(3)
13✔
339
        ttlSecondsAfterFinished := int32(3600) // Clean up after 1 hour
13✔
340

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

13✔
384
        if r.RendererCAConfigMap != "" {
26✔
385
                volumes = append(volumes, corev1.Volume{
13✔
386
                        Name: "ca-bundle",
13✔
387
                        VolumeSource: corev1.VolumeSource{
13✔
388
                                ConfigMap: &corev1.ConfigMapVolumeSource{
13✔
389
                                        LocalObjectReference: corev1.LocalObjectReference{
13✔
390
                                                Name: r.RendererCAConfigMap,
13✔
391
                                        },
13✔
392
                                        Items: []corev1.KeyToPath{
13✔
393
                                                {
13✔
394
                                                        Key:  "trust-bundle.pem",
13✔
395
                                                        Path: "ca-bundle.pem",
13✔
396
                                                },
13✔
397
                                        },
13✔
398
                                },
13✔
399
                        },
13✔
400
                })
13✔
401
                volumeMounts = append(volumeMounts, corev1.VolumeMount{
13✔
402
                        Name:      "ca-bundle",
13✔
403
                        MountPath: "/etc/ssl/certs",
13✔
404
                        ReadOnly:  true,
13✔
405
                })
13✔
406
                envVars = append(envVars, corev1.EnvVar{
13✔
407
                        Name:  "SSL_CERT_FILE",
13✔
408
                        Value: "/etc/ssl/certs/ca-bundle.pem",
13✔
409
                })
13✔
410
        }
13✔
411

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

13✔
455
        if authSecret != nil {
26✔
456
                switch authSecret.Type {
13✔
457
                case corev1.SecretTypeBasicAuth:
1✔
458
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1✔
459
                                corev1.EnvVar{
1✔
460
                                        Name: "REGISTRY_USERNAME",
1✔
461
                                        ValueFrom: &corev1.EnvVarSource{
1✔
462
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
463
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
464
                                                                Name: authSecret.Name,
1✔
465
                                                        },
1✔
466
                                                        Key: "username",
1✔
467
                                                },
1✔
468
                                        },
1✔
469
                                },
1✔
470
                                corev1.EnvVar{
1✔
471
                                        Name: "REGISTRY_PASSWORD",
1✔
472
                                        ValueFrom: &corev1.EnvVarSource{
1✔
473
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
474
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
475
                                                                Name: authSecret.Name,
1✔
476
                                                        },
1✔
477
                                                        Key: "password",
1✔
478
                                                },
1✔
479
                                        },
1✔
480
                                },
1✔
481
                        )
1✔
482

483
                case corev1.SecretTypeDockerConfigJson:
1✔
484
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1✔
485
                                Name: "dockerconfig",
1✔
486
                                VolumeSource: corev1.VolumeSource{
1✔
487
                                        Secret: &corev1.SecretVolumeSource{
1✔
488
                                                SecretName: authSecret.Name,
1✔
489
                                                Items: []corev1.KeyToPath{
1✔
490
                                                        {
1✔
491
                                                                Key:  ".dockerconfigjson",
1✔
492
                                                                Path: "dockerconfig.json",
1✔
493
                                                        },
1✔
494
                                                },
1✔
495
                                        },
1✔
496
                                },
1✔
497
                        })
1✔
498

1✔
499
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1✔
500
                                Name:      "dockerconfig",
1✔
501
                                MountPath: "/etc/renderer/dockerconfig.json",
1✔
502
                                SubPath:   "dockerconfig.json",
1✔
503
                                ReadOnly:  true,
1✔
504
                        })
1✔
505

1✔
506
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
1✔
507
                                Name:  "DOCKER_CONFIG",
1✔
508
                                Value: "/etc/renderer/dockerconfig.json",
1✔
509
                        })
1✔
510
                default:
11✔
511
                }
512
        }
513

514
        if err := r.Create(ctx, job); err != nil {
13✔
515
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
×
516
                return errLogAndWrap(log, err, "job creation failed")
×
517
        }
×
518

519
        // Set owner references
520
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
13✔
521
                return errLogAndWrap(log, err, "failed to set controller reference")
×
522
        }
×
523

524
        res.Status.JobRef = &corev1.ObjectReference{
13✔
525
                APIVersion: batchv1.SchemeGroupVersion.String(),
13✔
526
                Kind:       "Job",
13✔
527
                Namespace:  job.Namespace,
13✔
528
                Name:       job.Name,
13✔
529
        }
13✔
530

13✔
531
        if err := r.Status().Update(ctx, res); err != nil {
13✔
532
                return errLogAndWrap(log, err, "failed to update status")
×
533
        }
×
534

535
        return nil
13✔
536
}
537

538
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask) (*corev1.Secret, error) {
94✔
539
        log := ctrl.LoggerFrom(ctx)
94✔
540

94✔
541
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
94✔
542
        if err != nil {
94✔
543
                return nil, err
×
544
        }
×
545

546
        secret := &corev1.Secret{
94✔
547
                ObjectMeta: metav1.ObjectMeta{
94✔
548
                        Name:      r.configSecretKey(res).Name,
94✔
549
                        Namespace: r.configSecretKey(res).Namespace,
94✔
550
                        OwnerReferences: []metav1.OwnerReference{
94✔
551
                                {
94✔
552
                                        APIVersion:         solarv1alpha1.SchemeGroupVersion.String(),
94✔
553
                                        Kind:               res.Kind,
94✔
554
                                        Name:               res.Name,
94✔
555
                                        UID:                res.UID,
94✔
556
                                        Controller:         new(true),
94✔
557
                                        BlockOwnerDeletion: new(true),
94✔
558
                                },
94✔
559
                        },
94✔
560
                        Annotations: map[string]string{
94✔
561
                                annotationSecretName: r.configSecretKey(res).Name,
94✔
562
                        },
94✔
563
                },
94✔
564
                Type: corev1.SecretTypeOpaque,
94✔
565
                Data: map[string][]byte{
94✔
566
                        "config.json": cfgJson,
94✔
567
                },
94✔
568
        }
94✔
569

94✔
570
        if err := r.Create(ctx, secret); err != nil {
146✔
571
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
52✔
572
                return nil, errLogAndWrap(log, err, "secret creation failed")
52✔
573
        }
52✔
574

575
        // Set owner references
576
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
42✔
577
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
578
        }
×
579

580
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
42✔
581
                APIVersion: corev1.SchemeGroupVersion.String(),
42✔
582
                Kind:       "Secret",
42✔
583
                Namespace:  secret.Namespace,
42✔
584
                Name:       secret.Name,
42✔
585
        }
42✔
586

42✔
587
        if err := r.Status().Update(ctx, res); err != nil {
42✔
588
                return nil, errLogAndWrap(log, err, "failed to update status")
×
589
        }
×
590

591
        return secret, nil
42✔
592
}
593

594
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask) client.ObjectKey {
692✔
595
        return client.ObjectKey{
692✔
596
                Name:      fmt.Sprintf("render-%s", res.Name),
692✔
597
                Namespace: res.Namespace,
692✔
598
        }
692✔
599
}
692✔
600

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

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

615
// isJobComplete returns true if the Job is complete
616
func isJobComplete(job *batchv1.Job) bool {
85✔
617
        return job.Status.CompletionTime != nil
85✔
618
}
85✔
619

620
func (r *RenderTaskReconciler) referenceURL(repo string, tag string) string {
19✔
621
        base := r.BaseURL
19✔
622
        if !strings.HasPrefix(base, "oci://") {
38✔
623
                base = fmt.Sprintf("oci://%s", base)
19✔
624
        }
19✔
625
        base = strings.TrimSuffix(base, "/")
19✔
626
        url := fmt.Sprintf("%s/%s:%s", base, repo, tag)
19✔
627

19✔
628
        return url
19✔
629
}
630

631
// SetupWithManager sets up the controller with the Manager.
632
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
633
        return ctrl.NewControllerManagedBy(mgr).
1✔
634
                For(&solarv1alpha1.RenderTask{}).
1✔
635
                Owns(&batchv1.Job{}).
1✔
636
                Owns(&corev1.Secret{}).
1✔
637
                Complete(r)
1✔
638
}
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