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

opendefensecloud / solution-arsenal / 27414439904

12 Jun 2026 12:03PM UTC coverage: 74.245% (-0.2%) from 74.416%
27414439904

push

github

web-flow
feat: include e2e tests in CI (#575)

## What
Run e2e Tests in CI Pipelines
Closes #481 

## Why
Previously, the E2E test suite had to be triggered manually. To ensure
continuous integration and prevent regressions, these tests are now
automated within the main pipeline.
Additionally, the configuration includes adjustments for local execution
runners (`act`)

## Testing
- Local Verification: Verified the entire E2E suite locally using `act`
with privileged container options and host networking configurations.

## Notes for reviewers
- Local Dev Note: Running the suite locally via `act` now requires an
`.actrc` containing `--container-options "--privileged -v
/var/run/docker.sock:/var/run/docker.sock"` and `--network host`.

## Checklist
- [x] Tests added/updated
- [x] No breaking changes (or upgrade path documented above)
- [x] Readable commit history (squashed and cleaned up as desired)
- [x] AI code review considered and comments resolved


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* CI now exposes a reusable image-tag output and a dedicated end-to-end
test workflow; Makefile supports configurable registry/tag.

* **Tests**
* E2E runs consume produced image tags, create image-pull secrets when
needed, and include a new values fixture; CI vs local behavior is
handled.

* **Bug Fixes**
* Improved checks to avoid noisy grep output and ensure shellcheck is
available before linting.

* **Chores**
* Local/act runs skip signing, SBOM/provenance, QEMU and GHCR login for
faster iteration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

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

13 existing lines in 3 files now uncovered.

2952 of 3976 relevant lines covered (74.25%)

46.17 hits per line

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

86.33
/pkg/controller/rendertask_controller.go
1
// Copyright 2026 BWI GmbH and Solution Arsenal contributors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controller
5

6
import (
7
        "context"
8
        "encoding/json"
9
        "fmt"
10
        "strings"
11
        "time"
12

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

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

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

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

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

40
// RenderTaskReconciler reconciles a RenderTask object.
41
// Each RenderTask carries its own BaseURL and PushSecretRef for the target registry.
42
type RenderTaskReconciler struct {
43
        client.Client
44
        Scheme              *runtime.Scheme
45
        Recorder            events.EventRecorder
46
        RendererImage       string
47
        RendererCommand     string
48
        RendererArgs        []string
49
        RendererCAConfigMap string
50
        // WatchNamespace restricts reconciliation to this namespace.
51
        // Should be empty in production (watches all namespaces).
52
        // Intended for use in integration tests only.
53
        // See: https://book.kubebuilder.io/reference/envtest#testing-considerations
54
        WatchNamespace 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=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) {
792✔
66
        log := ctrl.LoggerFrom(ctx)
792✔
67
        ctrlResult := ctrl.Result{}
792✔
68

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

792✔
71
        if r.WatchNamespace != "" && req.Namespace != r.WatchNamespace {
829✔
72
                return ctrlResult, nil
37✔
73
        }
37✔
74

75
        // Fetch the RenderTask instance
76
        res := &solarv1alpha1.RenderTask{}
755✔
77
        if err := r.Get(ctx, req.NamespacedName, res); err != nil {
762✔
78
                if apierrors.IsNotFound(err) {
14✔
79
                        return ctrlResult, nil
7✔
80
                }
7✔
81

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

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

×
90
                return ctrlResult, nil
×
91
        }
×
92

93
        // Check if renderjob has already completed successfully
94
        sc := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobSucceeded)
748✔
95
        if sc != nil && sc.ObservedGeneration >= res.Generation && sc.Status == metav1.ConditionTrue {
752✔
96
                log.V(1).Info("RenderTask has already completed successfully, no further action needed")
4✔
97

4✔
98
                return ctrlResult, nil
4✔
99
        }
4✔
100

101
        // Determine the namespace for Jobs/Secrets — use the RenderTask's namespace
102
        jobNS := r.taskNamespace(res)
744✔
103

744✔
104
        // Reconcile Config Secret
744✔
105
        configSecret := &corev1.Secret{}
744✔
106
        err := r.Get(ctx, r.configSecretKey(res, jobNS), configSecret)
744✔
107
        if err != nil && apierrors.IsNotFound(err) {
1,255✔
108
                createdSecret, err := r.createConfigSecret(ctx, res, jobNS)
511✔
109
                if err != nil {
511✔
UNCOV
110
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateSecretFailed", "CreateConfigSecret", "Failed to create config secret: %s", err)
×
UNCOV
111

×
UNCOV
112
                        return ctrlResult, errLogAndWrap(log, err, "failed to create secret")
×
UNCOV
113
                }
×
114

115
                configSecret = createdSecret
511✔
116
        } else if err != nil {
233✔
117
                return ctrlResult, errLogAndWrap(log, err, "could not get secret")
×
118
        }
×
119

120
        // Resolve push secret from the RenderTask's PushSecretRef
121
        var pushSecret *corev1.Secret
744✔
122
        if res.Spec.PushSecretRef != nil {
1,488✔
123
                pushSecret = &corev1.Secret{}
744✔
124
                if err := r.Get(ctx, client.ObjectKey{Name: res.Spec.PushSecretRef.Name, Namespace: jobNS}, pushSecret); err != nil {
971✔
125
                        return ctrlResult, errLogAndWrap(log, err, "failed to get push secret")
227✔
126
                }
227✔
127
        }
128

129
        // Reconcile Job
130
        job := &batchv1.Job{}
517✔
131
        err = r.Get(ctx, r.renderJobKey(res, jobNS), job)
517✔
132
        if err != nil && apierrors.IsNotFound(err) {
533✔
133
                err := r.createRenderJob(ctx, res, configSecret, pushSecret, jobNS)
16✔
134
                if err != nil {
16✔
135
                        r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreateJobFailed", "CreateJob", "Failed to create job: %s", err)
×
136

×
137
                        return ctrlResult, errLogAndWrap(log, err, "failed to create job")
×
138
                }
×
139
        } else if err != nil {
501✔
140
                return ctrlResult, errLogAndWrap(log, err, "could not get job")
×
141
        }
×
142

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

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

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

3✔
157
                return ctrlResult, nil
3✔
158

159
        case job.Status.Failed > 0:
480✔
160
                if shouldCleanupSecrets(res, ttlDuration) {
956✔
161
                        cleanupSecrets(ctx, r, res, jobNS)
476✔
162
                        log.V(1).Info("Cleaned up secrets after failed job TTL")
476✔
163

476✔
164
                        return ctrlResult, nil
476✔
165
                }
476✔
166

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

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

173
        return ctrlResult, nil
34✔
174
}
175

176
// taskNamespace returns the namespace to use for Jobs/Secrets.
177
func (r *RenderTaskReconciler) taskNamespace(res *solarv1alpha1.RenderTask) string {
744✔
178
        return res.Namespace
744✔
179
}
744✔
180

181
// updateResourceStatusFromJob updates the resource status based on job status
182
func (r *RenderTaskReconciler) updateResourceStatusFromJob(ctx context.Context, res *solarv1alpha1.RenderTask, job *batchv1.Job) (changed bool) {
517✔
183
        log := ctrl.LoggerFrom(ctx)
517✔
184

517✔
185
        if job == nil {
517✔
186
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
×
187
                        Type:               ConditionTypeJobScheduled,
×
188
                        Status:             metav1.ConditionFalse,
×
189
                        ObservedGeneration: res.Generation,
×
190
                        Reason:             "DoesNotExist",
×
191
                        Message:            "Renderer job does not exist",
×
192
                })
×
193

×
194
                return changed
×
195
        }
×
196

197
        if job.Status.Succeeded > 0 {
520✔
198
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
3✔
199
                        Type:               ConditionTypeJobSucceeded,
3✔
200
                        Status:             metav1.ConditionTrue,
3✔
201
                        ObservedGeneration: res.Generation,
3✔
202
                        Reason:             "JobSucceeded",
3✔
203
                        Message:            fmt.Sprintf("Renderer job completed successfully at %v", job.Status.CompletionTime),
3✔
204
                })
3✔
205

3✔
206
                chartURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
3✔
207
                if res.Status.ChartURL != chartURL {
6✔
208
                        res.Status.ChartURL = chartURL
3✔
209
                        changed = true
3✔
210
                }
3✔
211

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

3✔
215
                return changed
3✔
216
        }
217

218
        if job.Status.Failed > 0 {
994✔
219
                changed = apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
480✔
220
                        Type:               ConditionTypeJobFailed,
480✔
221
                        Status:             metav1.ConditionTrue,
480✔
222
                        ObservedGeneration: res.Generation,
480✔
223
                        Reason:             "JobFailed",
480✔
224
                        Message:            "Renderer job failed",
480✔
225
                })
480✔
226
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "JobFailed", "RunJob", "Renderer job failed")
480✔
227
                log.V(1).Info("Job failed", "name", job.Name)
480✔
228

480✔
229
                return changed
480✔
230
        }
480✔
231

232
        return apimeta.SetStatusCondition(&res.Status.Conditions, metav1.Condition{
34✔
233
                Type:               ConditionTypeJobScheduled,
34✔
234
                Status:             metav1.ConditionTrue,
34✔
235
                ObservedGeneration: res.Generation,
34✔
236
                Reason:             "JobScheduled",
34✔
237
                Message:            fmt.Sprintf("Renderer job is running (active: %d, succeeded: %d, failed: %d)", job.Status.Active, job.Status.Succeeded, job.Status.Failed),
34✔
238
        })
34✔
239
}
240

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

247
        return r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
3✔
248
}
249

250
func (r *RenderTaskReconciler) deleteConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) error {
479✔
251
        secret := &corev1.Secret{}
479✔
252
        if err := r.Get(ctx, r.configSecretKey(res, jobNS), secret); err != nil {
479✔
253
                return err
×
254
        }
×
255

256
        return r.Delete(ctx, secret, client.PropagationPolicy(metav1.DeletePropagationBackground))
479✔
257
}
258

259
func (r *RenderTaskReconciler) createRenderJob(ctx context.Context, res *solarv1alpha1.RenderTask, configSecret, pushSecret *corev1.Secret, jobNS string) error {
16✔
260
        log := ctrl.LoggerFrom(ctx)
16✔
261

16✔
262
        jobKey := r.renderJobKey(res, jobNS)
16✔
263
        jobName := jobKey.Name
16✔
264
        backoffLimit := int32(3)
16✔
265
        ttlSecondsAfterFinished := int32(3600)
16✔
266
        if res.Spec.FailedJobTTL != nil {
18✔
267
                ttlSecondsAfterFinished = *res.Spec.FailedJobTTL
2✔
268
        }
2✔
269

270
        volumes := []corev1.Volume{
16✔
271
                {
16✔
272
                        Name: "config",
16✔
273
                        VolumeSource: corev1.VolumeSource{
16✔
274
                                Secret: &corev1.SecretVolumeSource{
16✔
275
                                        SecretName: configSecret.Name,
16✔
276
                                        Items: []corev1.KeyToPath{
16✔
277
                                                {
16✔
278
                                                        Key:  "config.json",
16✔
279
                                                        Path: "config.json",
16✔
280
                                                },
16✔
281
                                        },
16✔
282
                                },
16✔
283
                        },
16✔
284
                },
16✔
285
        }
16✔
286
        volumeMounts := []corev1.VolumeMount{
16✔
287
                {
16✔
288
                        Name:      "config",
16✔
289
                        MountPath: "/etc/renderer/config.json",
16✔
290
                        SubPath:   "config.json",
16✔
291
                        ReadOnly:  true,
16✔
292
                },
16✔
293
        }
16✔
294
        envVars := []corev1.EnvVar{
16✔
295
                {
16✔
296
                        Name: "POD_NAMESPACE",
16✔
297
                        ValueFrom: &corev1.EnvVarSource{
16✔
298
                                FieldRef: &corev1.ObjectFieldSelector{
16✔
299
                                        FieldPath: "metadata.namespace",
16✔
300
                                },
16✔
301
                        },
16✔
302
                },
16✔
303
                {
16✔
304
                        Name: "POD_NAME",
16✔
305
                        ValueFrom: &corev1.EnvVarSource{
16✔
306
                                FieldRef: &corev1.ObjectFieldSelector{
16✔
307
                                        FieldPath: "metadata.name",
16✔
308
                                },
16✔
309
                        },
16✔
310
                },
16✔
311
        }
16✔
312

16✔
313
        if r.RendererCAConfigMap != "" {
32✔
314
                volumes = append(volumes, corev1.Volume{
16✔
315
                        Name: "ca-bundle",
16✔
316
                        VolumeSource: corev1.VolumeSource{
16✔
317
                                ConfigMap: &corev1.ConfigMapVolumeSource{
16✔
318
                                        LocalObjectReference: corev1.LocalObjectReference{
16✔
319
                                                Name: r.RendererCAConfigMap,
16✔
320
                                        },
16✔
321
                                        Items: []corev1.KeyToPath{
16✔
322
                                                {
16✔
323
                                                        Key:  "trust-bundle.pem",
16✔
324
                                                        Path: "ca-bundle.pem",
16✔
325
                                                },
16✔
326
                                        },
16✔
327
                                },
16✔
328
                        },
16✔
329
                })
16✔
330
                volumeMounts = append(volumeMounts, corev1.VolumeMount{
16✔
331
                        Name:      "ca-bundle",
16✔
332
                        MountPath: "/etc/ssl/certs",
16✔
333
                        ReadOnly:  true,
16✔
334
                })
16✔
335
                envVars = append(envVars, corev1.EnvVar{
16✔
336
                        Name:  "SSL_CERT_FILE",
16✔
337
                        Value: "/etc/ssl/certs/ca-bundle.pem",
16✔
338
                })
16✔
339
        }
16✔
340

341
        pushURL := r.reference(res.Spec.BaseURL, res.Spec.Repository, res.Spec.Tag)
16✔
342

16✔
343
        job := &batchv1.Job{
16✔
344
                ObjectMeta: metav1.ObjectMeta{
16✔
345
                        Name:      jobName,
16✔
346
                        Namespace: jobKey.Namespace,
16✔
347
                        Annotations: map[string]string{
16✔
348
                                annotationJobName: jobName,
16✔
349
                        },
16✔
350
                },
16✔
351
                Spec: batchv1.JobSpec{
16✔
352
                        BackoffLimit:            &backoffLimit,
16✔
353
                        TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
16✔
354
                        Template: corev1.PodTemplateSpec{
16✔
355
                                Spec: corev1.PodSpec{
16✔
356
                                        RestartPolicy: corev1.RestartPolicyNever,
16✔
357
                                        Containers: []corev1.Container{
16✔
358
                                                {
16✔
359
                                                        Name:    "renderer",
16✔
360
                                                        Image:   r.RendererImage,
16✔
361
                                                        Command: []string{r.RendererCommand},
16✔
362
                                                        Args: append(r.RendererArgs,
16✔
363
                                                                "/etc/renderer/config.json",
16✔
364
                                                                fmt.Sprintf("--url=%s", pushURL),
16✔
365
                                                        ),
16✔
366
                                                        Env:          envVars,
16✔
367
                                                        VolumeMounts: volumeMounts,
16✔
368
                                                },
16✔
369
                                        },
16✔
370
                                        Volumes: volumes,
16✔
371
                                },
16✔
372
                        },
16✔
373
                },
16✔
374
        }
16✔
375

16✔
376
        if pushSecret != nil {
32✔
377
                switch pushSecret.Type {
16✔
378
                case corev1.SecretTypeBasicAuth:
1✔
379
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env,
1✔
380
                                corev1.EnvVar{
1✔
381
                                        Name: "REGISTRY_USERNAME",
1✔
382
                                        ValueFrom: &corev1.EnvVarSource{
1✔
383
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
384
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
385
                                                                Name: pushSecret.Name,
1✔
386
                                                        },
1✔
387
                                                        Key: "username",
1✔
388
                                                },
1✔
389
                                        },
1✔
390
                                },
1✔
391
                                corev1.EnvVar{
1✔
392
                                        Name: "REGISTRY_PASSWORD",
1✔
393
                                        ValueFrom: &corev1.EnvVarSource{
1✔
394
                                                SecretKeyRef: &corev1.SecretKeySelector{
1✔
395
                                                        LocalObjectReference: corev1.LocalObjectReference{
1✔
396
                                                                Name: pushSecret.Name,
1✔
397
                                                        },
1✔
398
                                                        Key: "password",
1✔
399
                                                },
1✔
400
                                        },
1✔
401
                                },
1✔
402
                        )
1✔
403

404
                case corev1.SecretTypeDockerConfigJson:
1✔
405
                        job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{
1✔
406
                                Name: "dockerconfig",
1✔
407
                                VolumeSource: corev1.VolumeSource{
1✔
408
                                        Secret: &corev1.SecretVolumeSource{
1✔
409
                                                SecretName: pushSecret.Name,
1✔
410
                                                Items: []corev1.KeyToPath{
1✔
411
                                                        {
1✔
412
                                                                Key:  ".dockerconfigjson",
1✔
413
                                                                Path: "dockerconfig.json",
1✔
414
                                                        },
1✔
415
                                                },
1✔
416
                                        },
1✔
417
                                },
1✔
418
                        })
1✔
419

1✔
420
                        job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
1✔
421
                                Name:      "dockerconfig",
1✔
422
                                MountPath: "/etc/renderer/dockerconfig.json",
1✔
423
                                SubPath:   "dockerconfig.json",
1✔
424
                                ReadOnly:  true,
1✔
425
                        })
1✔
426

1✔
427
                        job.Spec.Template.Spec.Containers[0].Env = append(job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{
1✔
428
                                Name:  "DOCKER_CONFIG",
1✔
429
                                Value: "/etc/renderer/dockerconfig.json",
1✔
430
                        })
1✔
431
                default:
14✔
432
                }
433
        }
434

435
        // Set owner references
436
        if err := controllerutil.SetControllerReference(res, job, r.Scheme); err != nil {
16✔
437
                return errLogAndWrap(log, err, "failed to set controller reference")
×
438
        }
×
439

440
        if err := r.Create(ctx, job); err != nil {
16✔
441
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create job: %s", err)
×
442

×
443
                return errLogAndWrap(log, err, "job creation failed")
×
444
        }
×
445

446
        res.Status.JobRef = &corev1.ObjectReference{
16✔
447
                APIVersion: batchv1.SchemeGroupVersion.String(),
16✔
448
                Kind:       "Job",
16✔
449
                Namespace:  job.Namespace,
16✔
450
                Name:       job.Name,
16✔
451
        }
16✔
452

16✔
453
        if err := r.Status().Update(ctx, res); err != nil {
16✔
454
                return errLogAndWrap(log, err, "failed to update status")
×
455
        }
×
456

457
        return nil
16✔
458
}
459

460
func (r *RenderTaskReconciler) createConfigSecret(ctx context.Context, res *solarv1alpha1.RenderTask, jobNS string) (*corev1.Secret, error) {
511✔
461
        log := ctrl.LoggerFrom(ctx)
511✔
462

511✔
463
        cfgJson, err := json.Marshal(res.Spec.RendererConfig)
511✔
464
        if err != nil {
511✔
465
                return nil, err
×
466
        }
×
467

468
        secretKey := r.configSecretKey(res, jobNS)
511✔
469
        secret := &corev1.Secret{
511✔
470
                ObjectMeta: metav1.ObjectMeta{
511✔
471
                        Name:      secretKey.Name,
511✔
472
                        Namespace: secretKey.Namespace,
511✔
473
                        Annotations: map[string]string{
511✔
474
                                annotationSecretName: secretKey.Name,
511✔
475
                        },
511✔
476
                },
511✔
477
                Type: corev1.SecretTypeOpaque,
511✔
478
                Data: map[string][]byte{
511✔
479
                        "config.json": cfgJson,
511✔
480
                },
511✔
481
        }
511✔
482

511✔
483
        // Set owner references
511✔
484
        if err := controllerutil.SetControllerReference(res, secret, r.Scheme); err != nil {
511✔
485
                return nil, errLogAndWrap(log, err, "failed to set controller reference")
×
486
        }
×
487

488
        if err := r.Create(ctx, secret); err != nil {
511✔
489
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "CreationFailed", "Create", "Failed to create secret: %s", err)
×
490

×
491
                return nil, errLogAndWrap(log, err, "secret creation failed")
×
492
        }
×
493

494
        res.Status.ConfigSecretRef = &corev1.ObjectReference{
511✔
495
                APIVersion: corev1.SchemeGroupVersion.String(),
511✔
496
                Kind:       "Secret",
511✔
497
                Namespace:  secret.Namespace,
511✔
498
                Name:       secret.Name,
511✔
499
        }
511✔
500

511✔
501
        if err := r.Status().Update(ctx, res); err != nil {
511✔
UNCOV
502
                return nil, errLogAndWrap(log, err, "failed to update status")
×
UNCOV
503
        }
×
504

505
        return secret, nil
511✔
506
}
507

508
func (r *RenderTaskReconciler) configSecretKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
1,734✔
509
        return client.ObjectKey{
1,734✔
510
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
1,734✔
511
                Namespace: jobNS,
1,734✔
512
        }
1,734✔
513
}
1,734✔
514

515
func (r *RenderTaskReconciler) renderJobKey(res *solarv1alpha1.RenderTask, jobNS string) client.ObjectKey {
536✔
516
        return client.ObjectKey{
536✔
517
                Name:      truncateName(fmt.Sprintf("render-%s", res.Name), maxK8sLabelValueLen),
536✔
518
                Namespace: jobNS,
536✔
519
        }
536✔
520
}
536✔
521

522
func (r *RenderTaskReconciler) reference(baseURL, repo, tag string) string {
19✔
523
        base := baseURL
19✔
524
        if !strings.HasPrefix(base, "oci://") {
38✔
525
                base = fmt.Sprintf("oci://%s", base)
19✔
526
        }
19✔
527

528
        base = strings.TrimSuffix(base, "/")
19✔
529

19✔
530
        return fmt.Sprintf("%s/%s:%s", base, repo, tag)
19✔
531
}
532

533
func ttlSeconds(ttl *int32) int32 {
517✔
534
        if ttl != nil {
999✔
535
                return *ttl
482✔
536
        }
482✔
537

538
        return 3600
35✔
539
}
540

541
func shouldCleanupSecrets(res *solarv1alpha1.RenderTask, ttl time.Duration) bool {
480✔
542
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
480✔
543

480✔
544
        return cond != nil && time.Since(cond.LastTransitionTime.Time) >= ttl
480✔
545
}
480✔
546

547
func remainingTTL(res *solarv1alpha1.RenderTask, ttl time.Duration) time.Duration {
4✔
548
        cond := apimeta.FindStatusCondition(res.Status.Conditions, ConditionTypeJobFailed)
4✔
549
        if cond == nil {
4✔
550
                return ttl
×
551
        }
×
552

553
        remaining := ttl - time.Since(cond.LastTransitionTime.Time)
4✔
554
        if remaining < 0 {
4✔
555
                return 0
×
556
        }
×
557

558
        return remaining
4✔
559
}
560

561
func cleanupSecrets(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, jobNS string) {
479✔
562
        if err := r.deleteConfigSecret(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
479✔
563
                r.Recorder.Eventf(res, nil, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete config secret: %s", err)
×
564
        }
×
565
}
566

567
func cleanupRenderResources(ctx context.Context, r *RenderTaskReconciler, res *solarv1alpha1.RenderTask, job *batchv1.Job, jobNS string) {
3✔
568
        cleanupSecrets(ctx, r, res, jobNS)
3✔
569
        if err := r.deleteRenderJob(ctx, res, jobNS); err != nil && !apierrors.IsNotFound(err) {
3✔
570
                r.Recorder.Eventf(res, job, corev1.EventTypeWarning, "DeletionFailed", "Delete", "Failed to delete job: %s", err)
×
571
        }
×
572
}
573

574
// SetupWithManager sets up the controller with the Manager.
575
func (r *RenderTaskReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
576
        return ctrl.NewControllerManagedBy(mgr).
1✔
577
                For(&solarv1alpha1.RenderTask{}).
1✔
578
                Owns(&batchv1.Job{}).
1✔
579
                Owns(&corev1.Secret{}).
1✔
580
                Complete(r)
1✔
581
}
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