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

opendefensecloud / solution-arsenal / 23052990701

13 Mar 2026 01:27PM UTC coverage: 71.23% (-0.8%) from 72.012%
23052990701

Pull #273

github

web-flow
Merge 93b67a195 into b27e7dfd8
Pull Request #273: Target e2e test

5 of 7 new or added lines in 2 files covered. (71.43%)

78 existing lines in 8 files now uncovered.

2102 of 2951 relevant lines covered (71.23%)

19.05 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
        "k8s.io/utils/ptr"
22
        ctrl "sigs.k8s.io/controller-runtime"
23
        "sigs.k8s.io/controller-runtime/pkg/client"
24
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
25

26
        solarv1alpha1 "go.opendefense.cloud/solar/api/solar/v1alpha1"
27
)
28

29
const (
30
        renderTaskFinalizer = "solar.opendefense.cloud/rendertask-finalizer"
31

32
        annotationJobName    = "solar.opendefense.cloud/job-name"
33
        annotationSecretName = "solar.opendefense.cloud/secret-name"
34

35
        // Condition types
36
        ConditionTypeJobScheduled = "JobScheduled"
37
        ConditionTypeJobSucceeded = "JobSucceeded"
38
        ConditionTypeJobFailed    = "JobFailed"
39

40
        ConditionTypeTaskCompleted = "TaskCompleted"
41
        ConditionTypeTaskFailed    = "TaskFailed"
42
)
43

44
// RenderTaskReconciler reconciles a RenderTask object
45
type RenderTaskReconciler struct {
46
        client.Client
47
        Scheme              *runtime.Scheme
48
        Recorder            events.EventRecorder
49
        RendererImage       string
50
        RendererCommand     string
51
        RendererArgs        []string
52
        PushSecretRef       *corev1.SecretReference
53
        BaseURL             string
54
        RendererCAConfigMap string
55
}
56

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

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

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

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

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

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

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

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

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

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

112
                return ctrlResult, nil
11✔
113
        }
114

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

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

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

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

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

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

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

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

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

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

215
        return ctrlResult, nil
×
216
}
217

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

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

×
231
                return changed
×
232
        }
×
233

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

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

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

3✔
251
                return changed
3✔
252
        }
253

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

1✔
265
                return changed
1✔
266
        }
1✔
267

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

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

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

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

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

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

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

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

61✔
307
        controllerSecret := &corev1.Secret{}
61✔
308
        if err := r.Get(ctx, client.ObjectKey{Name: r.PushSecretRef.Name, Namespace: r.PushSecretRef.Namespace}, controllerSecret); err != nil {
61✔
UNCOV
309
                return nil, err
×
UNCOV
310
        }
×
311

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

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

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

332
        return authSecret, nil
41✔
333
}
334

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

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

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

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

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

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

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

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

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

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

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

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

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

536
        return nil
41✔
537
}
538

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

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

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

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

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

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

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

592
        return secret, nil
43✔
593
}
594

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

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

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

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

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

47✔
629
        return url
47✔
630
}
631

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